import os import base64 import json import threading import time from datetime import datetime, timedelta from uuid import uuid4 import random import string import tempfile import io from PIL import Image, ImageOps from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename from dotenv import load_dotenv import requests load_dotenv() app = Flask(__name__) app.secret_key = 'super_secret_key_store_app_123_gippo_env' app.config.update( SESSION_COOKIE_SAMESITE='None', SESSION_COOKIE_SECURE=True, PERMANENT_SESSION_LIFETIME=timedelta(days=30) ) DATA_FILE = 'data.json' SYNC_FILES = [DATA_FILE] REPO_ID = os.getenv("REPO_ID", "Kgshop/fullmeta") HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") DEFAULT_WHATSAPP_NUMBER = "+77470623684" DEFAULT_LOGO_URL = "https://huggingface.co/spaces/Metapp/Tech/resolve/main/1776929812446-019db944-b5db-7524-8f44-73942d70a0f8.png" data_lock = threading.Lock() def get_almaty_time(): return (datetime.utcnow() + timedelta(hours=5)).strftime('%Y-%m-%d %H:%M:%S') def download_db_from_hf(specific_file=None, retries=3, delay=5): token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True for file_name in files_to_download: success = False for attempt in range(retries + 1): try: hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) success = True break except RepositoryNotFoundError: return False except HfHubHTTPError as e: if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: if file_name == DATA_FILE: with data_lock: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump({}, f) os.replace(temp_path, DATA_FILE) except Exception: pass success = False break except requests.exceptions.RequestException: pass except Exception: pass if attempt < retries: time.sleep(delay) if not success: all_successful = False return all_successful def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: return try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {get_almaty_time()}" ) except Exception: pass except Exception: pass def process_and_upload_image(file_obj, repo_path, size=(512, 512)): if not HF_TOKEN_WRITE: return None try: img = Image.open(file_obj).convert('RGB') img = ImageOps.fit(img, size, Image.Resampling.LANCZOS) buf = io.BytesIO() img.save(buf, format='JPEG', quality=85) buf.seek(0) filename = f"{uuid4().hex}.jpg" api = HfApi() api.upload_file( path_or_fileobj=buf, path_in_repo=f"{repo_path}/{filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) return filename except Exception: return None def periodic_backup(): while True: time.sleep(1800) upload_db_to_hf() def load_data(): with data_lock: data = {} try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) if not isinstance(data, dict): raise FileNotFoundError except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) except Exception: data = {} else: data = {} if 'products' in data or 'categories' in data: data = { 'default_env': { 'products': data.get('products', []), 'categories': data.get('categories',[]), 'category_photos': data.get('category_photos', {}), 'orders': data.get('orders', {}), 'staff': [], 'catalog_users': [], 'inventory_history':[], 'settings': { 'organization_name': 'Default Shop', 'admin_password_enabled': False, 'admin_password': '', 'logo_url': DEFAULT_LOGO_URL, 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, 'invoice_contacts': '', 'currency': 'T', 'track_inventory': False, 'use_barcodes': False, 'business_type': 'mixed', 'system_mode': 'both', 'hide_stock_online': False, 'closed_catalog_enabled': False, 'theme': 'light', 'customer_fields': { 'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False }, 'socials': { 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'}, 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'}, 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'} } } } } changed = False for env_id, env_data in data.items(): if 'products' not in env_data: env_data['products'] =[] if 'categories' not in env_data: env_data['categories'] =[] if 'category_photos' not in env_data: env_data['category_photos'] = {} if 'orders' not in env_data: env_data['orders'] = {} if 'staff' not in env_data: env_data['staff'] = [] if 'catalog_users' not in env_data: env_data['catalog_users'] = [] if 'inventory_history' not in env_data: env_data['inventory_history'] = [] if 'settings' not in env_data: env_data['settings'] = {} changed = True settings = env_data['settings'] if 'organization_name' not in settings: settings['organization_name'] = f'Shop {env_id}'; changed = True if 'admin_password_enabled' not in settings: settings['admin_password_enabled'] = False; changed = True if 'admin_password' not in settings: settings['admin_password'] = ''; changed = True if 'logo_url' not in settings: settings['logo_url'] = DEFAULT_LOGO_URL; changed = True if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True if 'invoice_contacts' not in settings: settings['invoice_contacts'] = ''; changed = True if 'currency' not in settings: settings['currency'] = 'T'; changed = True if 'track_inventory' not in settings: settings['track_inventory'] = False; changed = True if 'use_barcodes' not in settings: settings['use_barcodes'] = False; changed = True if 'business_type' not in settings: settings['business_type'] = 'mixed'; changed = True if 'system_mode' not in settings: settings['system_mode'] = 'both'; changed = True if 'hide_stock_online' not in settings: settings['hide_stock_online'] = False; changed = True if 'closed_catalog_enabled' not in settings: settings['closed_catalog_enabled'] = False; changed = True if 'theme' not in settings: settings['theme'] = 'light'; changed = True if 'customer_fields' not in settings: settings['customer_fields'] = {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False} changed = True if 'socials' not in settings: settings['socials'] = { 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'}, 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'}, 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'} } changed = True for product in env_data['products']: if 'product_id' not in product: product['product_id'] = uuid4().hex; changed = True if 'pieces_per_box' not in product: product['pieces_per_box'] = ""; changed = True if 'box_price' not in product: product['box_price'] = ""; changed = True if 'min_order' not in product: product['min_order'] = ""; changed = True if 'barcode' not in product: product['barcode'] = ""; changed = True if 'variants' not in product: product['variants'] =[]; changed = True if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True if 'stock' not in product: product['stock'] = ""; changed = True if 'is_available' not in product: product['is_available'] = True; changed = True if 'wholesale_tiers' not in product: product['wholesale_tiers'] = []; changed = True for v in product['variants']: if 'stock' not in v: v['stock'] = ""; changed = True if 'box_price' not in v: v['box_price'] = ""; changed = True if 'barcode' not in v: v['barcode'] = ""; changed = True if 'pieces_per_box' not in v: v['pieces_per_box'] = product.get('pieces_per_box', ""); changed = True if 'is_available' not in v: v['is_available'] = True; changed = True if 'wholesale_tiers' not in v: v['wholesale_tiers'] = []; changed = True for order_id, order in env_data['orders'].items(): if 'status' not in order: order['status'] = 'confirmed'; changed = True if 'staff_name' not in order: order['staff_name'] = ''; changed = True if 'assembled' not in order: order['assembled'] = {}; changed = True if 'global_discount' not in order: order['global_discount'] = 0; changed = True for item in order.get('cart', []): if 'discount' not in item: item['discount'] = 0; changed = True if 'category' not in item: item['category'] = 'Без категории'; changed = True if 'wholesale_tiers' not in item: item['wholesale_tiers'] = []; changed = True if changed or not os.path.exists(DATA_FILE): try: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as f: json.dump(data, f) os.replace(temp_path, DATA_FILE) except Exception: pass return data def save_data(data): try: if not isinstance(data, dict): return with data_lock: fd, temp_path = tempfile.mkstemp(dir=os.path.dirname(os.path.abspath(DATA_FILE)) or '.', text=True) with os.fdopen(fd, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) os.replace(temp_path, DATA_FILE) upload_db_to_hf(specific_file=DATA_FILE) except Exception: pass def get_env_data(env_id): all_data = load_data() if env_id not in all_data: all_data[env_id] = { 'products': [], 'categories':[], 'category_photos': {}, 'orders': {}, 'staff': [], 'catalog_users': [], 'inventory_history':[], 'settings': { 'organization_name': f'Shop {env_id}', 'admin_password_enabled': False, 'admin_password': '', 'logo_url': DEFAULT_LOGO_URL, 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, 'invoice_contacts': '', 'currency': 'T', 'track_inventory': False, 'use_barcodes': False, 'business_type': 'mixed', 'system_mode': 'both', 'hide_stock_online': False, 'closed_catalog_enabled': False, 'theme': 'light', 'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False}, 'socials': { 'wa': {'enabled': True, 'url': ''}, 'ig': {'enabled': True, 'url': ''}, 'tg': {'enabled': True, 'url': ''} } } } save_data(all_data) return all_data[env_id] def save_env_data(env_id, env_data): all_data = load_data() all_data[env_id] = env_data save_data(all_data) def update_order_totals(order, business_type): total = 0 global_discount = float(order.get('global_discount', 0)) for i in order['cart']: qty = int(i.get('quantity', 0)) if qty <= 0: continue ppb = int(i.get('pieces_per_box', 1)) c_price = float(i.get('price', 0)) c_box_price = float(i.get('cart_box_price', 0)) item_discount = float(i.get('discount', 0)) base_price = c_price tiers = i.get('wholesale_tiers', []) if business_type == 'wholesale' and tiers: valid_tiers = [t for t in tiers if qty >= t.get('qty', 0)] if valid_tiers: valid_tiers.sort(key=lambda x: x['qty'], reverse=True) base_price = float(valid_tiers[0]['price']) elif business_type in ['mixed', 'wholesale'] and c_box_price > 0 and ppb > 1 and qty >= ppb: base_price = c_box_price / ppb discounted_price = max(0, base_price - item_discount) item_total = discounted_price * qty i['calculated_price'] = round(discounted_price, 2) total += item_total total = max(0, total - global_discount) order['total_price'] = round(total, 2) def is_order_fully_assembled(order): if order.get('status') not in['confirmed', 'pos']: return True assembled_data = order.get('assembled', {}) for item in order.get('cart',[]): qty = int(item.get('quantity', 0)) if qty > 0: c_key = item.get('c_key') assembled_qty = int(assembled_data.get(c_key, 0)) if assembled_qty < qty: return False return True def deduct_stock(cart_items, products): for item in cart_items: pid = item.get('product_id') vidx = item.get('variant_idx', -1) qty = int(item.get('quantity', 0)) for p in products: if p['product_id'] == pid: if vidx != -1 and vidx < len(p.get('variants',[])): current_s = p['variants'][vidx].get('stock') if current_s != "" and current_s is not None: p['variants'][vidx]['stock'] = int(current_s) - qty else: current_s = p.get('stock') if current_s != "" and current_s is not None: p['stock'] = int(current_s) - qty break def restore_stock(c_key, pid, vidx, return_qty, products): for p in products: if p['product_id'] == pid: if vidx != -1 and vidx < len(p.get('variants',[])): current_s = p['variants'][vidx].get('stock') if current_s != "" and current_s is not None: p['variants'][vidx]['stock'] = int(current_s) + return_qty else: current_s = p.get('stock') if current_s != "" and current_s is not None: p['stock'] = int(current_s) + return_qty break def get_low_stock_items(products): low_stock =[] for p in products: if p.get('variants'): for vidx, v in enumerate(p['variants']): s = v.get('stock') if s != "" and s is not None and str(s).lstrip('-').isdigit() and int(s) < 100: low_stock.append({"name": p['name'], "variant": v.get('name'), "stock": int(s), "category": p.get('category', '')}) else: s = p.get('stock') if s != "" and s is not None and str(s).lstrip('-').isdigit() and int(s) < 100: low_stock.append({"name": p['name'], "variant": "", "stock": int(s), "category": p.get('category', '')}) return low_stock LANDING_PAGE_TEMPLATE = ''' MetaStore ''' LOGIN_TEMPLATE = ''' Вход

Вход

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' CATALOG_LOGIN_TEMPLATE = ''' Вход в каталог

Закрытый каталог

Введите 6-значный пароль для доступа

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' ADMHOSTO_TEMPLATE = ''' Управление

Среды

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
''' CATALOG_TEMPLATE = ''' {{ settings.organization_name }} {% if mode == 'pos' %}
Режим кассы: Быстрое оформление
{% endif %}

{{ settings.organization_name }}

{% if mode == 'pos' and staff_id %} {% endif %} {% if mode != 'pos' %} {% if settings.socials.wa.enabled and settings.socials.wa.url %} {% for link in settings.socials.wa.url.split() %} {% if link.strip() %} {% endif %} {% endfor %} {% endif %} {% if settings.socials.ig.enabled and settings.socials.ig.url %} {% for link in settings.socials.ig.url.split() %} {% if link.strip() %} {% endif %} {% endfor %} {% endif %} {% if settings.socials.tg.enabled and settings.socials.tg.url %} {% for link in settings.socials.tg.url.split() %} {% if link.strip() %} {% endif %} {% endfor %} {% endif %} {% endif %}
Сумма заказа: 0 {{ currency_code }}
''' ORDER_TEMPLATE = ''' Накладная №{{ order.id }}
{{ settings.organization_name }}
{% if settings.invoice_contacts %}
{% for contact in settings.invoice_contacts.split(',') %} {% if contact.strip() %}
{{ contact.strip() }}
{% endif %} {% endfor %}
{% endif %}

Накладная

№ {{ order.id }}
{{ order.created_at }}
{% if order.status != 'pos' and order.status != 'returned' %} {% if order.customer_name %}
Покупатель: {{ order.customer_name }}
{% endif %} {% if order.customer_phone %}
Телефон: {{ order.customer_phone }}
{% endif %} {% if order.customer_city %}
Город: {{ order.customer_city }}
{% endif %} {% if order.customer_address %}
Адрес: {{ order.customer_address }}
{% endif %} {% if order.customer_zip %}
Индекс: {{ order.customer_zip }}
{% endif %} {% else %}
Покупатель: {{ order.customer_name if order.customer_name else 'Касса (POS)' }}
{% if order.customer_whatsapp %}
WhatsApp: {{ order.customer_whatsapp }}
{% endif %} {% endif %} {% if order.staff_name %}
Сотрудник: {{ order.staff_name }}
{% endif %}
Статус: {% if order.status == 'pending' %} Ожидает подтверждения {% elif order.status == 'confirmed' %} Подтвержден {% elif order.status == 'pos' %} Выдан (Касса) {% elif order.status == 'returned' %} Возврат {% else %} {{ order.status }} {% endif %}
{% for item in order.cart %} {% set ppb = item.pieces_per_box|default(1)|int %} {% set boxes = item.quantity // ppb %} {% set remainder = item.quantity % ppb %} {% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %} {% if item.quantity > 0 %} {% endif %} {% endfor %}
Наименование Фото Кол-во Цена со скидкой Сумма
{{ loop.index }} {{ item.name }}
Категория: {{ item.category }}
{% if item.variant_name %}
Вариант: {{ item.variant_name }}
{% endif %}
Собрано: {{ assembled }} / {{ item.quantity }}
{% if item.discount and item.discount > 0 %}
Скидка: -{{ item.discount }} {{ currency_code }} за ед.
{% endif %}
img
{% if order.status == 'pending' %}
{% endif %}
{% if settings.business_type != 'retail' and ppb > 1 and boxes > 0 %} {{ boxes }} уп.{% if remainder > 0 %} {{ remainder }} шт.{% endif %} {% else %} {{ item.quantity }} шт. {% endif %}
{{ item.calculated_price | round(2) }} {{ (item.calculated_price * item.quantity) | round(2) }}
{% if order.global_discount > 0 %}
Применена общая скидка: -{{ order.global_discount }} {{ currency_code }}
{% endif %} Итого:
{{ order.total_price }} {{ currency_code }}
''' ASSEMBLY_TEMPLATE = ''' Сборка №{{ order.id }}
Назад

Сборка накладной № {{ order.id }}

{{ order.created_at }}
{% for item in order.cart %} {% if item.quantity > 0 %} {% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %}
{{ item.name }}
{% if item.variant_name %}
Вариант: {{ item.variant_name }}
{% endif %}
Нужно: {{ item.quantity }} шт.
{% if assembled >= item.quantity %}Собрано{% else %}В процессе{% endif %}
{% endif %} {% endfor %}
''' HISTORY_TEMPLATE = ''' {{ page_title }}

{{ page_title }}

Назад в панель
Период:
Номер Дата Клиент / Сотрудник Сумма ({{ currency_code }}) Статус Действия
''' ADMIN_TEMPLATE = ''' Админ-панель
{% set sys_mode = settings.system_mode|default('both') %}

Админ-панель ({{ env_id }})

{% if sys_mode != 'light_external' %} Отчеты {% endif %} {% if sys_mode not in ['external', 'light_external'] %} Остатки {% if low_stock_count > 0 %}{{ low_stock_count }}{% endif %} {% endif %} {% if sys_mode == 'both' %} История накладных и заказов {% elif sys_mode in ['external', 'light_external'] %} История заказов {% else %} История накладных {% endif %} {% if sys_mode != 'light_external' %} Сборка {% if unassembled_count > 0 %}{{ unassembled_count }}{% endif %} {% endif %} {% if sys_mode != 'internal' %} В каталог {% endif %} {% if settings.admin_password_enabled %} Выход {% endif %}
Онлайн заказы {% if pending_orders|length > 0 %}{{ pending_orders|length }}{% endif %}
{% if pending_orders %} {% for order in pending_orders %}
Заказ №{{ order.id }} {{ order.created_at }}
Клиент: {{ order.customer_name }} ({{ order.customer_phone }})
{% if order.staff_name %}
Сотрудник: {{ order.staff_name }}
{% endif %}
Сумма: {{ order.total_price }} {{ currency_code }}
Накладная
{% endfor %} {% else %}

Нет новых онлайн заказов

{% endif %}
{% if sys_mode != 'internal' %}
Пользователи (Закрытый каталог)
{% for u in catalog_users %}
{{ u.name }} Пароль: {{ u.password }}
{% endfor %}
{% endif %} {% if sys_mode != 'light_external' %}
Персонал
{% for s in staff %}
{{ s.name }} ({{ s.whatsapp }})
{% if sys_mode != 'internal' %} {% endif %} {% if sys_mode != 'external' %} {% endif %}
{% endfor %}
{% endif %}
Настройка магазина
Текущий логотип:
Сброс данных:

Управление категориями

{% for category in categories %}
{{ category }}
{% if category_photos.get(category) %}
{% endif %}
Добавить товар
Новый товар в категории "{{ category }}"
{% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type != 'retail' %}
{% endif %} {% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type == 'wholesale' %}
{% endif %}
{% for product in products %} {% if product.category == category %}
{% if product.photos and product.photos|length > 0 %} {% else %}
{% endif %}
{{ product.name }}
ID: {{ product.product_id }}
{% if product.description %} {{ product.description[:50] }}{{ '...' if product.description|length > 50 else '' }} {% endif %} {% if product.is_available == False %} Нет в наличии • {% endif %} {% if product.has_variant_prices %} Цена по вариантам {% else %} {{ product.price }} {{ currency_code }} {% if settings.business_type == 'mixed' and product.box_price %} (Уп: {{ product.box_price }}){% endif %} {% endif %} {% if settings.business_type != 'retail' %} • В уп: {{ product.pieces_per_box|default(1) }} шт {% endif %} {% if settings.business_type == 'wholesale' and product.min_order %} • Мин: {{ product.min_order }} шт {% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %} {% if not product.variants %} • Остаток: {{ product.stock if product.stock != "" else "0" }} {% else %} • Остаток по вариантам {% endif %} {% endif %}
Редактирование товара
{% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type != 'retail' %}
{% endif %} {% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type == 'wholesale' %}
{% for tier in product.wholesale_tiers %}
{% endfor %}
{% endif %}
{% for variant in product.variants %}
{% if settings.business_type != 'retail' %}
{% endif %} {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
{% endif %}
{% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% for tier in variant.wholesale_tiers %}
{% endfor %}
{% endif %}
{% endfor %}
{% if product.photos and product.photos|length > 0 %}
{% for ph in product.photos %}
{% endfor %}
{% endif %}
{% endif %} {% endfor %}
{% endfor %}
''' REPORTS_TEMPLATE = ''' Отчеты

Расширенные Отчеты

Назад в панель
Период:
Общий отчет
По дням
По категориям
По сотрудникам
Общая выручка
0
Кол-во заказов
0
Средний чек (AOV)
0
Продано товаров (шт)
0
Возвраты (сумма)
0

Топ продаваемых товаров

Товар Кол-во (шт) Сумма ({{ currency_code }})

Продажи по дням

Дата Кол-во заказов Выручка ({{ currency_code }})

Продажи по категориям

Категория Кол-во (шт) Выручка ({{ currency_code }})

Выручка по сотрудникам

Сотрудник Кол-во заказов Выручка ({{ currency_code }})
''' INVENTORY_TEMPLATE = ''' Учет остатков

Учет остатков

Назад в панель
Текущие остатки
Заканчивающиеся {{ low_stock_items|length }}
История операций
{% for p in products %} {% if not p.variants %} {% else %} {% for v in p.variants %} {% endfor %} {% endif %} {% endfor %}
Товар Категория Остаток Действие (Приход / Списание)
{{ p.name }}
ID: {{ p.product_id }}
{{ p.category }} {{ p.stock if p.stock != "" else "0" }}
{{ p.name }} ({{ v.name }})
ID: {{ p.product_id }}
{{ p.category }} {{ v.stock if v.stock != "" else "0" }}
{% for item in low_stock_items %} {% else %} {% endfor %}
Товар Вариант Категория Остаток
{{ item.name }} {{ item.variant }} {{ item.category }} {{ item.stock }}
Нет заканчивающихся товаров
{% for h in history|reverse %} {% else %} {% endfor %}
Дата Товар Тип Кол-во Комментарий
{{ h.date }} {{ h.product }} {% if h.variant %}({{ h.variant }}){% endif %} {% if h.change > 0 %} Приход {% else %} Списание {% endif %} {{ h.change|abs }} {{ h.comment }}
История пуста
''' @app.route('/') def index(): return render_template_string(LANDING_PAGE_TEMPLATE) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() environments_data =[] for env_id, env_data in data.items(): if env_id == 'default_env': continue settings = env_data.get('settings', {}) org_name = settings.get("organization_name", f"Shop {env_id}") environments_data.append({ "id": env_id, "org_name": org_name, "pwd_enabled": settings.get("admin_password_enabled", False), "password": settings.get("admin_password", ""), "system_mode": settings.get("system_mode", "both") }) environments_data.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) @app.route('/admhosto/create', methods=['POST']) def create_environment(): all_data = load_data() while True: new_id = ''.join(random.choices(string.digits, k=6)) if new_id not in all_data: break all_data[new_id] = { 'products': [], 'categories':[], 'category_photos': {}, 'orders': {}, 'staff': [], 'catalog_users': [], 'inventory_history':[], 'settings': { "organization_name": f"Shop {new_id}", "admin_password_enabled": False, "admin_password": "", "logo_url": DEFAULT_LOGO_URL, "whatsapp_number": DEFAULT_WHATSAPP_NUMBER, "invoice_contacts": "", "currency": "T", "track_inventory": False, "use_barcodes": False, "business_type": "mixed", "system_mode": "both", "hide_stock_online": False, "closed_catalog_enabled": False, "theme": "light", "customer_fields": {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False}, "socials": { 'wa': {'enabled': True, 'url': ''}, 'ig': {'enabled': True, 'url': ''}, 'tg': {'enabled': True, 'url': ''} } } } save_data(all_data) flash(f'Новая среда с ID {new_id} успешно создана.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/update_pwd/', methods=['POST']) def update_env_pwd(env_id): all_data = load_data() if env_id in all_data: pwd_enabled = 'pwd_enabled' in request.form password = request.form.get('password', '').strip() all_data[env_id]['settings']['admin_password_enabled'] = pwd_enabled all_data[env_id]['settings']['admin_password'] = password save_data(all_data) flash(f'Пароль для среды {env_id} обновлен.', 'success') else: flash(f'Среда {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/update_mode/', methods=['POST']) def update_env_mode(env_id): all_data = load_data() if env_id in all_data: mode = request.form.get('system_mode', 'both') all_data[env_id]['settings']['system_mode'] = mode if mode in ['external', 'light_external']: all_data[env_id]['settings']['track_inventory'] = False all_data[env_id]['settings']['use_barcodes'] = False all_data[env_id]['settings']['closed_catalog_enabled'] = False save_data(all_data) flash(f'Режим среды {env_id} обновлен.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/clear_history/', methods=['POST']) def clear_history(env_id): if request.form.get('pwd') == 'admin': all_data = load_data() if env_id in all_data: all_data[env_id]['orders'] = {} all_data[env_id]['inventory_history'] =[] save_data(all_data) flash(f'История среды {env_id} успешно очищена.', 'success') else: flash('Неверный пароль для очистки истории.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/delete/', methods=['POST']) def delete_environment(env_id): all_data = load_data() if env_id in all_data: del all_data[env_id] save_data(all_data) flash(f'Среда {env_id} была удалена.', 'success') else: flash(f'Среда {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('//login', methods=['GET', 'POST']) def admin_login(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if not settings.get('admin_password_enabled'): return redirect(url_for('admin', env_id=env_id)) if request.method == 'POST': pwd = request.form.get('password', '') if pwd == settings.get('admin_password', ''): session.permanent = True session[f'admin_auth_{env_id}'] = True return redirect(url_for('admin', env_id=env_id)) else: flash('Неверный пароль', 'error') return render_template_string(LOGIN_TEMPLATE, env_id=env_id) @app.route('//logout') def admin_logout(env_id): session.pop(f'admin_auth_{env_id}', None) return redirect(url_for('admin_login', env_id=env_id)) @app.route('//catalog', methods=['GET', 'POST']) def catalog(env_id): data = get_env_data(env_id) all_products = data.get('products',[]) categories = data.get('categories',[]) category_photos = data.get('category_photos', {}) settings = data.get('settings', {}) mode = request.args.get('mode', 'online') staff_id = request.args.get('staff_id', '') if settings.get('system_mode', 'both') == 'internal' and mode != 'pos': return "Каталог недоступен в режиме внутреннего учета", 403 if settings.get('closed_catalog_enabled') and mode != 'pos': if not session.get(f'catalog_auth_{env_id}'): if request.method == 'POST': pwd = request.form.get('password', '').strip() catalog_users = data.get('catalog_users', []) valid = any(u.get('password') == pwd for u in catalog_users) if valid: session.permanent = True session[f'catalog_auth_{env_id}'] = True return redirect(url_for('catalog', env_id=env_id, mode=mode, staff_id=staff_id)) else: flash('Неверный пароль', 'error') return render_template_string(CATALOG_LOGIN_TEMPLATE, env_id=env_id) return render_template_string( CATALOG_TEMPLATE, products_json=json.dumps(all_products), categories_json=json.dumps(categories), category_photos_json=json.dumps(category_photos), repo_id=REPO_ID, currency_code=settings.get('currency', 'T'), settings=settings, env_id=env_id, mode=mode, staff_id=staff_id ) @app.route('//api/staff_orders/') def get_staff_orders(env_id, staff_id): data = get_env_data(env_id) orders = data.get('orders', {}) date_filter = request.args.get('date') staff_orders =[o for o in orders.values() if o.get('staff_id') == staff_id and (o.get('status') == 'pos' or o.get('status') == 'confirmed')] if date_filter: filtered =[] for o in staff_orders: if o.get('created_at', '').startswith(date_filter): filtered.append(o) staff_orders = filtered staff_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True) return jsonify(staff_orders[:50]) @app.route('//api/assembly/', methods=['POST']) def update_assembly(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return jsonify({"success": False, "error": "Order not found"}), 404 req = request.json c_key = req.get('c_key') qty = int(req.get('qty', 0)) if 'assembled' not in order: order['assembled'] = {} order['assembled'][c_key] = qty save_env_data(env_id, data) return jsonify({"success": True}) @app.route('//api/finish_assembly/', methods=['POST']) def finish_assembly(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return jsonify({"success": False}), 404 assembled_data = order.get('assembled', {}) new_cart = [] track_inv = data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') not in ['external', 'light_external'] is_stock_deducted = order.get('status') in ['confirmed', 'pos'] for item in order.get('cart',[]): c_key = item.get('c_key') original_qty = item.get('quantity', 0) assembled_qty = int(assembled_data.get(c_key, 0)) if assembled_qty < original_qty and is_stock_deducted and track_inv: diff = original_qty - assembled_qty restore_stock(c_key, item.get('product_id'), item.get('variant_idx', -1), diff, data['products']) if assembled_qty > 0: item['quantity'] = assembled_qty new_cart.append(item) order['cart'] = new_cart update_order_totals(order, data['settings'].get('business_type', 'mixed')) save_env_data(env_id, data) return jsonify({"success": True}) @app.route('//process_return/', methods=['POST']) def process_return(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return jsonify({"success": False, "error": "Order not found"}), 404 req_data = request.get_json() returns = req_data.get('returns', {}) track_inv = data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') not in['external', 'light_external'] for c_key, ret_qty in returns.items(): ret_qty = int(ret_qty) if ret_qty <= 0: continue for item in order['cart']: if item.get('c_key') == c_key: if ret_qty > item['quantity']: ret_qty = item['quantity'] item['quantity'] -= ret_qty if track_inv: restore_stock(c_key, item.get('product_id'), item.get('variant_idx', -1), ret_qty, data['products']) break update_order_totals(order, data['settings'].get('business_type', 'mixed')) if order['total_price'] <= 0: order['status'] = 'returned' save_env_data(env_id, data) return jsonify({"success": True}) @app.route('//create_order', methods=['POST']) def create_order(env_id): order_data = request.get_json() if not order_data or 'cart' not in order_data: return jsonify({"error": "Bad request"}), 400 data = get_env_data(env_id) cart_items = order_data['cart'] mode = order_data.get('mode', 'online') staff_id = order_data.get('staff_id', '') global_discount = float(order_data.get('global_discount', 0)) if global_discount < 0: global_discount = 0 staff_name = '' staff_whatsapp = '' if staff_id: for s in data.get('staff', []): if s['id'] == staff_id: staff_name = s['name'] staff_whatsapp = s['whatsapp'] break order_status = 'pos' if mode == 'pos' else 'pending' customer_name = order_data.get('customer_name', '') customer_phone = order_data.get('customer_phone', '') customer_city = order_data.get('customer_city', '') customer_address = order_data.get('customer_address', '') customer_zip = order_data.get('customer_zip', '') customer_whatsapp = order_data.get('customer_whatsapp', '') product_dict = {p['product_id']: p.get('category', 'Без категории') for p in data.get('products', [])} processed_cart =[] for item in cart_items: processed_cart.append({ "c_key": item.get('c_key'), "product_id": item.get('product_id'), "name": item['name'], "price": float(item['cart_price']), "cart_box_price": float(item.get('cart_box_price', 0)), "quantity": int(item['quantity']), "pieces_per_box": int(item.get('pieces_per_box', 1)) if str(item.get('pieces_per_box', 1)).strip() != "" else 1, "variant_name": item.get('variant_name', ''), "variant_idx": item.get('variant_idx', -1), "discount": float(item.get('discount', 0)), "wholesale_tiers": item.get('wholesale_tiers', []), "category": product_dict.get(item.get('product_id'), 'Без категории'), "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+" }) order_id = f"SA-{datetime.now().strftime('%Y%m%d')}-{str(len(data.get('orders', {}))+1).zfill(3)}" new_order = { "id": order_id, "created_at": get_almaty_time(), "cart": processed_cart, "status": order_status, "staff_id": staff_id, "staff_name": staff_name, "staff_whatsapp": staff_whatsapp, "customer_name": customer_name, "customer_phone": customer_phone, "customer_city": customer_city, "customer_address": customer_address, "customer_zip": customer_zip, "customer_whatsapp": customer_whatsapp, "global_discount": global_discount, "assembled": {} } update_order_totals(new_order, data['settings'].get('business_type', 'mixed')) if order_status == 'pos' and data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') not in ['external', 'light_external']: deduct_stock(processed_cart, data['products']) data['orders'][order_id] = new_order save_env_data(env_id, data) return jsonify({"order_id": order_id}), 201 @app.route('//order/') def view_order(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) settings = data.get('settings', {}) if not order: return "Order not found", 404 return render_template_string( ORDER_TEMPLATE, order=order, settings=settings, currency_code=settings.get('currency', 'T'), env_id=env_id ) @app.route('//assembly/') def view_assembly(env_id, order_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('system_mode', 'both') == 'light_external': return redirect(url_for('admin', env_id=env_id)) order = data.get('orders', {}).get(order_id) if not order: return "Order not found", 404 return render_template_string( ASSEMBLY_TEMPLATE, order=order, env_id=env_id ) @app.route('//edit_order/', methods=['POST']) def edit_order(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return jsonify({"success": False, "error": "Order not found"}), 404 if order.get('status') != 'pending': return jsonify({"success": False, "error": "Can only edit pending orders"}), 400 req_data = request.get_json() c_key = req_data.get('c_key') change = req_data.get('change', 0) exact_qty = req_data.get('exact_qty') remove = req_data.get('remove', False) for item in order['cart']: if item.get('c_key') == c_key: if remove: order['cart'].remove(item) else: if exact_qty is not None: item['quantity'] = int(exact_qty) else: item['quantity'] += change if item['quantity'] <= 0: order['cart'].remove(item) break update_order_totals(order, data['settings'].get('business_type', 'mixed')) save_env_data(env_id, data) return jsonify({"success": True, "total_price": order['total_price']}) @app.route('//apply_discount/', methods=['POST']) def apply_discount(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return redirect(url_for('admin', env_id=env_id)) if order.get('status') != 'pending': flash('Скидку можно применить только к ожидающим заказам.', 'error') return redirect(url_for('admin', env_id=env_id)) global_discount = request.form.get('global_discount', 0) try: order['global_discount'] = float(global_discount) except ValueError: order['global_discount'] = 0 for item in order.get('cart',[]): item_disc = request.form.get(f"item_discount_{item['c_key']}", 0) try: item['discount'] = float(item_disc) except ValueError: item['discount'] = 0 update_order_totals(order, data['settings'].get('business_type', 'mixed')) save_env_data(env_id, data) flash('Скидка успешно применена.', 'success') return redirect(url_for('admin', env_id=env_id)) @app.route('//order_action/', methods=['POST']) def order_action(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if not order: return redirect(url_for('admin', env_id=env_id)) action = request.form.get('action') if action == 'confirm' and order.get('status') == 'pending': order['status'] = 'confirmed' if data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') not in ['external', 'light_external']: deduct_stock(order['cart'], data['products']) elif action == 'delete': del data['orders'][order_id] save_env_data(env_id, data) return redirect(url_for('admin', env_id=env_id)) @app.route('//history') def history(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) orders_list = list(data.get('orders', {}).values()) orders_list.sort(key=lambda x: x.get('created_at', ''), reverse=True) sys_mode = settings.get('system_mode', 'both') if sys_mode in ['external', 'light_external']: page_title = 'История заказов' elif sys_mode == 'internal': page_title = 'История накладных' else: page_title = 'История заказов и накладных' return render_template_string( HISTORY_TEMPLATE, env_id=env_id, sys_mode=sys_mode, currency_code=settings.get('currency', 'T'), orders_json=json.dumps(orders_list), page_title=page_title ) @app.route('//assembly_list') def assembly_list(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) sys_mode = settings.get('system_mode', 'both') if sys_mode == 'light_external': return redirect(url_for('admin', env_id=env_id)) all_orders = list(data.get('orders', {}).values()) unassembled =[o for o in all_orders if not is_order_fully_assembled(o)] unassembled.sort(key=lambda x: x.get('created_at', ''), reverse=True) return render_template_string( HISTORY_TEMPLATE, env_id=env_id, sys_mode=sys_mode, currency_code=settings.get('currency', 'T'), orders_json=json.dumps(unassembled), page_title='Сборка (Несобранные заказы)' ) @app.route('//reports') def reports(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) if settings.get('system_mode', 'both') == 'light_external': return redirect(url_for('admin', env_id=env_id)) orders_list = list(data.get('orders', {}).values()) return render_template_string( REPORTS_TEMPLATE, env_id=env_id, currency_code=settings.get('currency', 'T'), orders_json=json.dumps(orders_list) ) @app.route('//inventory') def inventory(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) if settings.get('system_mode', 'both') in ['external', 'light_external']: return redirect(url_for('admin', env_id=env_id)) low_stock_items = get_low_stock_items(data.get('products',[])) return render_template_string( INVENTORY_TEMPLATE, env_id=env_id, products=data.get('products', []), history=data.get('inventory_history',[]), low_stock_items=low_stock_items) @app.route('//api/inventory', methods=['POST']) def api_inventory(env_id): data = get_env_data(env_id) req = request.json pid = req.get('product_id') vidx = int(req.get('variant_idx', -1)) qty = int(req.get('qty', 0)) is_add = req.get('is_add', True) comment = req.get('comment', '') for p in data['products']: if p['product_id'] == pid: name = p['name'] v_name = "" if vidx != -1 and vidx < len(p.get('variants',[])): v_name = p['variants'][vidx]['name'] curr = p['variants'][vidx].get('stock', 0) curr = int(curr) if str(curr).lstrip('-').strip() != "" and curr is not None else 0 p['variants'][vidx]['stock'] = curr + qty if is_add else curr - qty else: curr = p.get('stock', 0) curr = int(curr) if str(curr).lstrip('-').strip() != "" and curr is not None else 0 p['stock'] = curr + qty if is_add else curr - qty data['inventory_history'].append({ "date": get_almaty_time(), "product": name, "variant": v_name, "change": qty if is_add else -qty, "comment": comment }) save_env_data(env_id, data) return jsonify({"success": True}) return jsonify({"success": False}) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'): return redirect(url_for('admin_login', env_id=env_id)) products = data.get('products',[]) categories = data.get('categories',[]) category_photos = data.get('category_photos', {}) staff = data.get('staff',[]) catalog_users = data.get('catalog_users', []) orders = data.get('orders', {}) pending_orders =[o for o in orders.values() if o.get('status') == 'pending'] pending_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True) unassembled_count = len([o for o in orders.values() if not is_order_fully_assembled(o)]) low_stock_count = len(get_low_stock_items(products)) if request.method == 'POST': action = request.form.get('action') if action == 'add_staff': staff_name = request.form.get('staff_name', '').strip() staff_wa = request.form.get('staff_whatsapp', '').strip() if staff_name and staff_wa: staff.append({'id': uuid4().hex, 'name': staff_name, 'whatsapp': staff_wa}) data['staff'] = staff save_env_data(env_id, data) elif action == 'delete_staff': sid = request.form.get('staff_id') data['staff'] = [s for s in staff if s['id'] != sid] save_env_data(env_id, data) elif action == 'add_catalog_user': user_name = request.form.get('user_name', '').strip() if user_name: pwd = ''.join(random.choices(string.digits, k=6)) catalog_users.append({'id': uuid4().hex, 'name': user_name, 'password': pwd}) data['catalog_users'] = catalog_users save_env_data(env_id, data) elif action == 'delete_catalog_user': uid = request.form.get('user_id') data['catalog_users'] = [u for u in catalog_users if u['id'] != uid] save_env_data(env_id, data) elif action == 'clear_history': data['orders'] = {} data['inventory_history'] =[] save_env_data(env_id, data) flash('История продаж и заказов успешно очищена.', 'success') elif action == 'update_settings': settings['organization_name'] = request.form.get('organization_name', '').strip() settings['theme'] = request.form.get('theme', 'light') settings['business_type'] = request.form.get('business_type', 'mixed') settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip() settings['invoice_contacts'] = request.form.get('invoice_contacts', '').strip() settings['currency'] = request.form.get('currency', 'T') if settings.get('system_mode', 'both') in ['external', 'light_external']: settings['track_inventory'] = False settings['use_barcodes'] = False settings['hide_stock_online'] = False settings['closed_catalog_enabled'] = 'closed_catalog_enabled' in request.form else: settings['track_inventory'] = 'track_inventory' in request.form settings['use_barcodes'] = 'use_barcodes' in request.form settings['hide_stock_online'] = 'hide_stock_online' in request.form settings['closed_catalog_enabled'] = 'closed_catalog_enabled' in request.form settings['admin_password_enabled'] = 'admin_password_enabled' in request.form settings['admin_password'] = request.form.get('admin_password', '').strip() settings['customer_fields'] = { 'name': 'cf_name' in request.form, 'phone': 'cf_phone' in request.form, 'city': 'cf_city' in request.form, 'address': 'cf_address' in request.form, 'zip': 'cf_zip' in request.form } logo_file = request.files.get('logo') if logo_file and logo_file.filename and HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) ext = os.path.splitext(logo_file.filename)[1].lower() if ext in['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg']: logo_filename = f"logo_{uuid4().hex}{ext}" temp_path = os.path.join(uploads_dir, logo_filename) logo_file.save(temp_path) try: api = HfApi() api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"logos/{logo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) settings['logo_url'] = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/logos/{logo_filename}" except Exception: pass finally: if os.path.exists(temp_path): os.remove(temp_path) settings['socials']['wa']['enabled'] = 'wa_enabled' in request.form settings['socials']['wa']['url'] = request.form.get('wa_url', '').strip() settings['socials']['ig']['enabled'] = 'ig_enabled' in request.form settings['socials']['ig']['url'] = request.form.get('ig_url', '').strip() settings['socials']['tg']['enabled'] = 'tg_enabled' in request.form settings['socials']['tg']['url'] = request.form.get('tg_url', '').strip() data['settings'] = settings save_env_data(env_id, data) elif action == 'add_category': cat_name = request.form.get('category_name', '').strip() if cat_name and cat_name not in categories: categories.append(cat_name) data['categories'] = categories photo_file = request.files.get('category_photo') if photo_file and photo_file.filename: filename = process_and_upload_image(photo_file, 'category_photos', size=(512, 512)) if filename: data.setdefault('category_photos', {})[cat_name] = filename save_env_data(env_id, data) elif action == 'edit_category': old_cat = request.form.get('old_category_name', '') new_cat = request.form.get('new_category_name', '').strip() if old_cat and new_cat and old_cat in categories: if old_cat != new_cat and new_cat not in categories: idx = categories.index(old_cat) categories[idx] = new_cat for p in products: if p.get('category') == old_cat: p['category'] = new_cat if old_cat in data.get('category_photos', {}): data.setdefault('category_photos', {})[new_cat] = data['category_photos'].pop(old_cat) cat_to_update = new_cat if (old_cat != new_cat and new_cat not in categories) else old_cat if old_cat == new_cat or (old_cat != new_cat and new_cat not in categories): photo_file = request.files.get('category_photo') if photo_file and photo_file.filename: filename = process_and_upload_image(photo_file, 'category_photos', size=(512, 512)) if filename: data.setdefault('category_photos', {})[cat_to_update] = filename data['categories'] = categories data['products'] = products save_env_data(env_id, data) elif action == 'move_category': cat_name = request.form.get('category_name') direction = request.form.get('direction') if cat_name in categories: idx = categories.index(cat_name) if direction == 'up' and idx > 0: categories[idx], categories[idx-1] = categories[idx-1], categories[idx] elif direction == 'down' and idx < len(categories) - 1: categories[idx], categories[idx+1] = categories[idx+1], categories[idx] data['categories'] = categories save_env_data(env_id, data) elif action == 'delete_category': cat_name = request.form.get('category_name') if cat_name in categories: categories.remove(cat_name) data['products'] =[p for p in products if p.get('category') != cat_name] data['categories'] = categories if cat_name in data.get('category_photos', {}): del data['category_photos'][cat_name] save_env_data(env_id, data) elif action == 'add_product': name = request.form.get('name', '').strip() barcode = request.form.get('barcode', '').strip() is_available = request.form.get('is_available', '1') == '1' price_str = request.form.get('price', '') price = float(price_str) if price_str else "" ppb_str = request.form.get('pieces_per_box', '') pieces_per_box = int(ppb_str) if ppb_str else "" bp_str = request.form.get('box_price', '') box_price = float(bp_str) if bp_str else "" moq_str = request.form.get('min_order', '') min_order = int(moq_str) if moq_str else "" stock_str = request.form.get('stock', '') main_stock = int(stock_str) if stock_str else "" description = request.form.get('description', '').strip() category = request.form.get('category') has_variant_prices = 'has_variant_prices' in request.form main_wholesale_tiers = [] if not has_variant_prices: mt_qtys = request.form.getlist('main_tier_qty[]') mt_prices = request.form.getlist('main_tier_price[]') for q, p_val in zip(mt_qtys, mt_prices): if q and p_val: main_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)}) variant_names = request.form.getlist('variant_name[]') variant_barcodes = request.form.getlist('variant_barcode[]') variant_prices = request.form.getlist('variant_price[]') variant_box_prices = request.form.getlist('variant_box_price[]') variant_stocks = request.form.getlist('variant_stock[]') variant_ppbs = request.form.getlist('variant_pieces_per_box[]') variant_is_availables = request.form.getlist('variant_is_available[]') variants = [] for i in range(len(variant_names)): v_name = variant_names[i].strip() if v_name: v_price = price v_box_price = box_price v_is_avail = True if i < len(variant_is_availables): v_is_avail = variant_is_availables[i] == '1' v_wholesale_tiers = [] if has_variant_prices: if i < len(variant_prices) and variant_prices[i]: v_price = float(variant_prices[i]) if i < len(variant_box_prices) and variant_box_prices[i]: v_box_price = float(variant_box_prices[i]) vt_qtys = request.form.getlist(f'variant_{i}_tier_qty[]') vt_prices = request.form.getlist(f'variant_{i}_tier_price[]') for q, p_val in zip(vt_qtys, vt_prices): if q and p_val: v_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)}) v_stock = "" if i < len(variant_stocks) and variant_stocks[i]: v_stock = int(variant_stocks[i]) v_barcode = "" if i < len(variant_barcodes) and variant_barcodes[i]: v_barcode = variant_barcodes[i].strip() v_ppb = pieces_per_box if i < len(variant_ppbs) and variant_ppbs[i]: v_ppb = int(variant_ppbs[i]) variants.append({ "name": v_name, "barcode": v_barcode, "price": v_price, "box_price": v_box_price, "stock": v_stock, "pieces_per_box": v_ppb, "is_available": v_is_avail, "wholesale_tiers": v_wholesale_tiers }) uploaded_photos = request.files.getlist('photos')[:10] photos_list =[] if uploaded_photos and HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) api = HfApi() for photo in uploaded_photos: if photo and photo.filename: ext = os.path.splitext(photo.filename)[1].lower() if ext not in['.jpg', '.jpeg', '.png', '.webp', '.gif']: continue photo_filename = f"{uuid4().hex}{ext}" temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) try: api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) photos_list.append(photo_filename) except Exception: pass finally: if os.path.exists(temp_path): os.remove(temp_path) new_product = { 'product_id': uuid4().hex, 'name': name, 'barcode': barcode, 'price': price, 'pieces_per_box': pieces_per_box, 'box_price': box_price, 'min_order': min_order, 'stock': main_stock, 'description': description, 'category': category, 'photos': photos_list, 'variants': variants, 'has_variant_prices': has_variant_prices, 'is_available': is_available, 'wholesale_tiers': main_wholesale_tiers } products.append(new_product) data['products'] = products save_env_data(env_id, data) elif action == 'edit_product': pid = request.form.get('product_id') name = request.form.get('name', '').strip() barcode = request.form.get('barcode', '').strip() is_available = request.form.get('is_available', '1') == '1' price_str = request.form.get('price', '') price = float(price_str) if price_str else "" ppb_str = request.form.get('pieces_per_box', '') pieces_per_box = int(ppb_str) if ppb_str else "" bp_str = request.form.get('box_price', '') box_price = float(bp_str) if bp_str else "" moq_str = request.form.get('min_order', '') min_order = int(moq_str) if moq_str else "" stock_str = request.form.get('stock', '') main_stock = int(stock_str) if stock_str else "" description = request.form.get('description', '').strip() has_variant_prices = 'has_variant_prices' in request.form main_wholesale_tiers = [] if not has_variant_prices: mt_qtys = request.form.getlist('main_tier_qty[]') mt_prices = request.form.getlist('main_tier_price[]') for q, p_val in zip(mt_qtys, mt_prices): if q and p_val: main_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)}) remove_photos = request.form.getlist('remove_photos[]') variant_names = request.form.getlist('variant_name[]') variant_barcodes = request.form.getlist('variant_barcode[]') variant_prices = request.form.getlist('variant_price[]') variant_box_prices = request.form.getlist('variant_box_price[]') variant_stocks = request.form.getlist('variant_stock[]') variant_ppbs = request.form.getlist('variant_pieces_per_box[]') variant_is_availables = request.form.getlist('variant_is_available[]') variants = [] for i in range(len(variant_names)): v_name = variant_names[i].strip() if v_name: v_price = price v_box_price = box_price v_is_avail = True if i < len(variant_is_availables): v_is_avail = variant_is_availables[i] == '1' v_wholesale_tiers = [] if has_variant_prices: if i < len(variant_prices) and variant_prices[i]: v_price = float(variant_prices[i]) if i < len(variant_box_prices) and variant_box_prices[i]: v_box_price = float(variant_box_prices[i]) vt_qtys = request.form.getlist(f'variant_{i}_tier_qty[]') vt_prices = request.form.getlist(f'variant_{i}_tier_price[]') for q, p_val in zip(vt_qtys, vt_prices): if q and p_val: v_wholesale_tiers.append({'qty': int(q), 'price': float(p_val)}) v_stock = "" if i < len(variant_stocks) and variant_stocks[i]: v_stock = int(variant_stocks[i]) v_barcode = "" if i < len(variant_barcodes) and variant_barcodes[i]: v_barcode = variant_barcodes[i].strip() v_ppb = pieces_per_box if i < len(variant_ppbs) and variant_ppbs[i]: v_ppb = int(variant_ppbs[i]) variants.append({ "name": v_name, "barcode": v_barcode, "price": v_price, "box_price": v_box_price, "stock": v_stock, "pieces_per_box": v_ppb, "is_available": v_is_avail, "wholesale_tiers": v_wholesale_tiers }) uploaded_photos = request.files.getlist('photos')[:10] photos_list =[] if uploaded_photos and uploaded_photos[0].filename and HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) api = HfApi() for photo in uploaded_photos: if photo and photo.filename: ext = os.path.splitext(photo.filename)[1].lower() if ext not in['.jpg', '.jpeg', '.png', '.webp', '.gif']: continue photo_filename = f"{uuid4().hex}{ext}" temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) try: api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) photos_list.append(photo_filename) except Exception: pass finally: if os.path.exists(temp_path): os.remove(temp_path) for p in products: if p.get('product_id') == pid: p['name'] = name p['barcode'] = barcode p['price'] = price p['pieces_per_box'] = pieces_per_box p['box_price'] = box_price p['min_order'] = min_order p['stock'] = main_stock p['description'] = description p['variants'] = variants p['has_variant_prices'] = has_variant_prices p['is_available'] = is_available p['wholesale_tiers'] = main_wholesale_tiers existing_photos = p.get('photos', []) existing_photos =[ph for ph in existing_photos if ph not in remove_photos] p['photos'] = existing_photos + photos_list break data['products'] = products save_env_data(env_id, data) elif action == 'move_product': pid = request.form.get('product_id') direction = request.form.get('direction') idx = -1 for i, p in enumerate(products): if p.get('product_id') == pid: idx = i break if idx != -1: cat = products[idx].get('category') if direction == 'up': swap_idx = -1 for i in range(idx - 1, -1, -1): if products[i].get('category') == cat: swap_idx = i break if swap_idx != -1: products[idx], products[swap_idx] = products[swap_idx], products[idx] elif direction == 'down': swap_idx = -1 for i in range(idx + 1, len(products)): if products[i].get('category') == cat: swap_idx = i break if swap_idx != -1: products[idx], products[swap_idx] = products[swap_idx], products[idx] data['products'] = products save_env_data(env_id, data) elif action == 'delete_product': pid = request.form.get('product_id') data['products'] =[p for p in products if p.get('product_id') != pid] save_env_data(env_id, data) return redirect(url_for('admin', env_id=env_id)) return render_template_string( ADMIN_TEMPLATE, products=products, categories=categories, category_photos=category_photos, repo_id=REPO_ID, currency_code=settings.get('currency', 'T'), env_id=env_id, settings=settings, staff=staff, catalog_users=catalog_users, pending_orders=pending_orders, unassembled_count=unassembled_count, low_stock_count=low_stock_count ) @app.route('//force_upload', methods=['POST']) def force_upload(env_id): upload_db_to_hf() return redirect(url_for('admin', env_id=env_id)) @app.route('//force_download', methods=['POST']) def force_download(env_id): download_db_from_hf() return redirect(url_for('admin', env_id=env_id)) if __name__ == '__main__': download_db_from_hf() load_data() if HF_TOKEN_WRITE: threading.Thread(target=periodic_backup, daemon=True).start() port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port)