import os
import io
import base64
import json
import logging
import threading
import time
import math
from datetime import datetime, timedelta, timezone
from uuid import uuid4
import random
import string
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session
from PIL import Image
import google.generativeai as genai
import numpy as np
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 = 'your_unique_secret_key_gippo_312_shop_54321_no_login'
DATA_FILE = 'data.json'
SYNC_FILES =[DATA_FILE]
REPO_ID = "Kgshop/metas"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
ALMATY_TZ = timezone(timedelta(hours=6))
db_lock = threading.RLock()
CURRENCIES = {
'KGS': 'Кыргызский сом',
'KZT': 'Казахстанский тенге',
'UAH': 'Украинская гривна',
'RUB': 'Российский рубль',
'USD': 'Доллар США',
'EUR': 'Евро'
}
COLOR_SCHEMES = {
'default': 'Бирюзовый (по умолч.)',
'forest': 'Лесной зеленый',
'ocean': 'Глубокий синий',
'sunset': 'Закатный оранжевый',
'lavender': 'Лавандовый',
'vintage': 'Винтажный',
'dark': 'Полночь (тёмная)',
'cosmic': 'Космическая ночь',
'minty': 'Свежая мята',
'mocha': 'Кофейный мокко',
'crimson': 'Багровый рассвет',
'solar': 'Солнечная вспышка',
'cyberpunk': 'Киберпанк неон',
'neon': 'Неоновая вспышка',
'pastel': 'Пастельный (светлый)',
'emerald': 'Изумрудный город',
'gold': 'Роскошное золото',
'sakura': 'Цветение сакуры (светлый)',
'arctic': 'Арктический лед (светлый)',
'volcano': 'Магма',
'monochrome_light': 'Классика (Светлая)',
'monochrome_dark': 'Классика (Темная)',
'nord': 'Скандинавский Норд',
'dracula': 'Дракула (Темный)',
'ruby': 'Глубокий Рубин',
'sapphire': 'Королевский Сапфир',
'amethyst': 'Аметистовый блеск'
}
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
pass
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:
local_path = 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 open(file_name, 'w', encoding='utf-8') as f:
json.dump({}, f)
except Exception:
pass
success = False
break
else:
pass
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} {datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')}"
)
except Exception:
pass
except Exception:
pass
def periodic_backup():
backup_interval = 1800
while True:
time.sleep(backup_interval)
upload_db_to_hf()
def load_data():
with db_lock:
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, dict):
data = {}
except (FileNotFoundError, json.JSONDecodeError):
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
if not isinstance(data, dict):
data = {}
except (FileNotFoundError, json.JSONDecodeError):
data = {}
else:
data = {}
return data
def save_data(data):
with db_lock:
try:
with open(DATA_FILE, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=4)
except Exception:
pass
upload_db_to_hf(specific_file=DATA_FILE)
def get_env_data(env_id):
with db_lock:
all_data = load_data()
default_organization_info = {
"about_us": "Мы — надежный партнер в мире уникальных товаров.",
"shipping": "Доставка осуществляется по всему Кыргызстану.",
"returns": "Возврат и обмен товара возможен в течение 14 дней.",
"contact": "Наш магазин находится по адресу: Рынок Кербен. Мы работаем ежедневно с 9:00 до 18:00."
}
default_settings = {
"organization_name": "Gippo312",
"whatsapp_number": "+996701202013",
"currency_code": "KGS",
"chat_name": "EVA",
"chat_avatar": None,
"color_scheme": "default",
"business_type": "retail",
"env_mode": "external",
"welcome_message_enabled": False,
"welcome_message_text": "Добро пожаловать в наш магазин!",
"inventory_tracking": False,
"admin_password_enabled": False,
"admin_password": "",
"checkout_fields_enabled": False,
"checkout_fields": {"name": False, "phone": False, "city": False, "address": False, "zip": False},
"categories_as_lines": False
}
env_data = all_data.get(env_id, {})
if not env_data:
env_data = {
'products': [], 'categories':[], 'orders': {}, 'employees':[], 'blocks':[],
'organization_info': default_organization_info,
'settings': default_settings,
'inventory_history':[]
}
if 'products' not in env_data: env_data['products'] =[]
if 'categories' not in env_data: env_data['categories'] =[]
if 'orders' not in env_data: env_data['orders'] = {}
if 'organization_info' not in env_data: env_data['organization_info'] = default_organization_info
if 'settings' not in env_data: env_data['settings'] = default_settings
if 'employees' not in env_data: env_data['employees'] =[]
if 'blocks' not in env_data: env_data['blocks'] =[]
if 'inventory_history' not in env_data: env_data['inventory_history'] =[]
settings_changed = False
for key, value in default_settings.items():
if key not in env_data['settings']:
env_data['settings'][key] = value
settings_changed = True
products_changed = False
for product in env_data['products']:
if 'product_id' not in product:
product['product_id'] = uuid4().hex
products_changed = True
if 'views' not in product:
product['views'] = 0
products_changed = True
if 'tags' not in product:
product['tags'] =[]
products_changed = True
else:
for tag in product['tags']:
if 'stock' not in tag:
tag['stock'] = 0
products_changed = True
if 'stock_batches' not in tag:
tag['stock_batches'] = [{"qty": tag['stock'], "price": tag.get('price', 0), "box_price": tag.get('box_price', 0)}]
products_changed = True
if products_changed or settings_changed:
save_env_data(env_id, env_data)
return env_data
def save_env_data(env_id, env_data):
with db_lock:
all_data = load_data()
all_data[env_id] = env_data
save_data(all_data)
def configure_gemini():
if not GOOGLE_API_KEY:
return False
try:
genai.configure(api_key=GOOGLE_API_KEY)
return True
except Exception:
return False
def generate_ai_description_from_image(image_data, language):
if not configure_gemini():
raise ValueError("Google AI API не настроен.")
try:
if not image_data:
raise ValueError("Файл изображения не найден.")
image_stream = io.BytesIO(image_data)
image = Image.open(image_stream).convert('RGB')
except Exception:
raise ValueError("Не удалось обработать изображение.")
base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона."
lang_suffix = ""
if language == "Русский":
lang_suffix = " Пиши на русском языке."
elif language == "Кыргызский":
lang_suffix = " Пиши на кыргызском языке."
elif language == "Казахский":
lang_suffix = " Пиши на казахском языке."
elif language == "Узбекский":
lang_suffix = " Пиши на узбекском языке."
final_prompt = f"{base_prompt}{lang_suffix}"
try:
model = genai.GenerativeModel('gemma-4-31b-it')
response = model.generate_content([final_prompt, image])
if hasattr(response, 'text'):
return response.text
else:
if response.parts:
return "".join(part.text for part in response.parts if hasattr(part, 'text'))
else:
response.resolve()
return response.text
except Exception as e:
raise ValueError(f"Ошибка при генерации контента: {e}")
LANDING_PAGE_TEMPLATE = '''
MetaStore - AI система для Вашего Бизнеса
'''
LOGIN_TEMPLATE = '''
Вход в Админ-панель
Вход
{% 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 %}
Существующие среды
{% if environments %}
{% for env in environments %}
{% endfor %}
{% else %}
Пока не создано ни одной среды.
{% endif %}
'''
CATALOG_TEMPLATE = '''
{{ settings.organization_name }} - Каталог
{% if blocks %}
{% for block in blocks %}
{% if block.type == 'link' %}
{{ block.title }}
{% elif block.type == 'text' %}
{% if block.title %}
{{ block.title }} {% endif %}
{{ block.content|replace('\\n', ' ')|safe }}
{% endif %}
{% endfor %}
{% endif %}
0
'''
PRODUCT_DETAIL_TEMPLATE = '''
{{ product['name'] }}
{% if product.get('photos') and product['photos']|length > 0 %}
{% for photo in product['photos'] %}
{% set photo_idx = loop.index0 %}
{% for tag in product.tags %}
{% if tag.photo_index == photo_idx %}
{% endif %}
{% endfor %}
{% endfor %}
{% else %}
{% endif %}
{% if product.get('photos') and product['photos']|length > 1 %}
{% endif %}
Категория: {{ product.get('category', 'Без категории') }}
Описание: {{ product.get('description', 'Описание отсутствует.')|replace('\\n', ' ')|safe }}
'''
ORDER_TEMPLATE = '''
Заказ №{{ order.id }} - {{ settings.organization_name }}
{% if order %}
Ваш Заказ №{{ order.id }}
Дата создания: {{ order.created_at }}
{% if order.customer_data %}
Данные клиента
{% if order.customer_data.name %}
Имя: {{ order.customer_data.name }}
{% endif %}
{% if order.customer_data.phone %}
Телефон: {{ order.customer_data.phone }}
{% endif %}
{% if order.customer_data.city %}
Город: {{ order.customer_data.city }}
{% endif %}
{% if order.customer_data.address %}
Адрес: {{ order.customer_data.address }}
{% endif %}
{% if order.customer_data.zip %}
Индекс: {{ order.customer_data.zip }}
{% endif %}
{% endif %}
Товары в заказе
{% for item in order.cart %}
{{ item.name }}
{{ "%.2f"|format(item.price) }} {{ currency_code }}
{% if item.discount_applied %}
(Оптовая цена)
{% endif %}
{% endfor %}
ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}
Статус заказа
{% if order.employee_name %}
Ваш персональный менеджер: {{ order.employee_name }}
{% endif %}
Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.
Отправить в WhatsApp
Печать накладной
Печать таблицей
← Вернуться в каталог
Заказ №{{ order.id }}
Дата: {{ order.created_at }}
№
Наименование
Кол-во
Цена
Сумма
{% for item in order.cart %}
{{ loop.index }}
{{ item.name }}
{{ item.quantity }}
{{ "%.2f"|format(item.price) }}
{{ "%.2f"|format(item.price * item.quantity) }}
{% endfor %}
ИТОГО:
{{ "%.2f"|format(order.total_price) }}
{% else %}
Ошибка
Заказ с таким ID не найден.
← Вернуться в каталог
{% endif %}
'''
HISTORY_TEMPLATE = '''
История Продаж - {{ settings.organization_name }}
'''
POS_TEMPLATE = '''
POS Касса - {{ settings.organization_name }}
0
'''
REPORTS_TEMPLATE = '''
Отчеты - {{ settings.organization_name }}
Назад в админ-панель
Отчеты (Режим 2 в 1)
Всего заказов
{{ total_orders }}
Общая выручка
{{ "%.2f"|format(total_revenue) }} {{ currency_code }}
Заказы с кассы (POS)
{{ pos_orders }}
Онлайн заказы
{{ online_orders }}
Продажи по сотрудникам
Сотрудник Кол-во заказов Выручка
{% for emp, data in emp_stats.items() %}
{{ emp }}
{{ data.count }}
{{ "%.2f"|format(data.revenue) }} {{ currency_code }}
{% endfor %}
Топ продаваемых товаров
Название товара Продано шт.
{% for item in top_products %}
{{ item.name }}
{{ item.qty }}
{% endfor %}
'''
INVENTORY_TEMPLATE = '''
Остатки - {{ settings.organization_name }}
'''
ADMIN_TEMPLATE = '''
Админ-панель - {{ settings.organization_name }}
Сохранение данных...
Пожалуйста, подождите, идет обработка и загрузка.
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
Сотрудники (Менеджеры)
Добавить сотрудника
{% if employees %}
{% for emp in employees %}
{{ emp.name }} {{ emp.whatsapp }}
Ссылка менеджера
{% if settings.env_mode == '2in1' %}
Ссылка на кассу
{% endif %}
{% endfor %}
{% else %}
Сотрудники не добавлены.
{% endif %}
Блоки в каталоге (Ссылки и Инфо)
Добавить блок
{% if blocks %}
{% for block in blocks %}
{{ block.title }} ({{ 'Ссылка' if block.type == 'link' else 'Текст' }})
{% if block.type == 'link' %}
{{ block.url }} {% endif %}
{% endfor %}
{% else %}
Блоки не добавлены.
{% endif %}
Управление категориями
Добавить новую категорию
Название новой категории:
Добавить
Существующие категории:
{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}
Категорий пока нет.
{% endif %}
Информация о магазине
Развернуть/Свернуть
О нас: {{ organization_info.get('about_us', '') }}
Доставка: {{ organization_info.get('shipping', '') }}
Возврат: {{ organization_info.get('returns', '') }}
Контакты: {{ organization_info.get('contact', '') }}
Сохранить
Управление товарами
Добавить новый товар
Список товаров:
Поиск
{% if search_q %}
Сброс
{% endif %}
{% if paginated_products %}
{% for product in paginated_products %}
{% if product.get('photos') %}
{% else %}
{% endif %}
{{ product['name'] }}
{% if product.get('in_stock', True) %}
В наличии
{% else %}
Нет
{% endif %}
Категория: {{ product.get('category', 'Без категории') }}
{% set min_price = 0 %}
{% if product.tags %}
{% if settings.business_type == 'wholesale' %}
{% set prices = product.tags | map(attribute='box_price') | list %}
{% else %}
{% set prices = product.tags | map(attribute='price') | list %}
{% endif %}
{% if prices %}
{% set min_price = prices | min %}
{% endif %}
{% endif %}
Цена от: {% if min_price > 0 %}{{ "%.2f"|format(min_price) }} {{ currency_code }}{% else %}Не указана{% endif %}
{% if product.get('tags') %}
Отметок: {{ product.tags|length }}
{% endif %}
Просмотров: {{ product.get('views', 0) }}
Редактировать
Удалить
{% endfor %}
{% if total_pages > 1 %}
{% endif %}
{% else %}
Товаров пока нет или по вашему запросу ничего не найдено.
{% endif %}
'''
@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():
settings = env_data.get('settings', {})
org_name = settings.get("organization_name", "Gippo312")
env_mode = settings.get("env_mode", "external")
environments_data.append({
"id": env_id,
"org_name": org_name,
"mode": env_mode,
"pwd_enabled": settings.get("admin_password_enabled", False),
"password": settings.get("admin_password", "")
})
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()
env_mode = request.form.get('env_mode', 'external')
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':[], 'orders': {}, 'employees':[], 'blocks':[],
'organization_info': {
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.",
"shipping": "Доставка осуществляется по всему Кыргызстану.",
"returns": "Возврат и обмен товара возможен в течение 14 дней.",
"contact": "Наш магазин находится по адресу: ... Связаться с нами можно по телефону ..."
},
'settings': {
"organization_name": "Gippo312", "whatsapp_number": "+996701202013",
"currency_code": "KGS", "chat_name": "EVA", "chat_avatar": None,
"color_scheme": "default",
"business_type": "retail", "env_mode": env_mode, "welcome_message_enabled": False, "welcome_message_text": "Добро пожаловать в наш магазин!",
"inventory_tracking": False, "admin_password_enabled": False, "admin_password": "",
"checkout_fields_enabled": False, "checkout_fields": {"name": False, "phone": False, "city": False, "address": False, "zip": False},
"categories_as_lines": False
},
'inventory_history':[]
}
save_data(all_data)
flash(f'Новая среда с ID {new_id} успешно создана.', 'success')
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:
new_mode = request.form.get('env_mode', 'external')
all_data[env_id]['settings']['env_mode'] = new_mode
save_data(all_data)
flash(f'Режим среды {env_id} обновлен.', 'success')
else:
flash(f'Среда {env_id} не найдена.', 'error')
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/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[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))
def update_tag_price_from_batches(tag):
if 'stock_batches' in tag:
for batch in tag['stock_batches']:
if batch.get('qty', 0) > 0:
tag['price'] = batch.get('price', tag.get('price'))
tag['box_price'] = batch.get('box_price', tag.get('box_price'))
break
@app.route('//catalog')
def catalog(env_id):
data = get_env_data(env_id)
all_products_raw = data.get('products',[])
settings = data.get('settings', {})
blocks = data.get('blocks',[])
env_mode = settings.get('env_mode', 'external')
product_categories = set(p.get('category', 'Без категории') for p in all_products_raw)
admin_categories = set(data.get('categories',[]))
all_cat_names = sorted(list(product_categories.union(admin_categories)))
products_in_stock =[]
for p in all_products_raw:
if not p.get('in_stock', True):
continue
if env_mode == '2in1':
valid_tags =[t for t in p.get('tags', []) if t.get('stock', 0) > 0]
if not valid_tags and p.get('tags',[]):
continue
p_copy = p.copy()
p_copy['tags'] = valid_tags
products_in_stock.append(p_copy)
else:
products_in_stock.append(p)
products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
products_by_category = {cat:[] for cat in all_cat_names}
for product in products_in_stock:
products_by_category[product.get('category', 'Без категории')].append(product)
ordered_categories =[cat for cat in all_cat_names if products_by_category.get(cat)]
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
return render_template_string(
CATALOG_TEMPLATE, ordered_categories=ordered_categories,
products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'),
settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, blocks=blocks
)
@app.route('//pos')
def pos_page(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('env_mode') != '2in1':
return "POS доступен только в режиме '2 в 1'", 403
emp_id = request.args.get('emp', '')
all_products_raw = data.get('products',[])
products_in_stock =[]
for p in all_products_raw:
if not p.get('in_stock', True):
continue
valid_tags =[t for t in p.get('tags', []) if t.get('stock', 0) > 0]
if not valid_tags and p.get('tags',[]):
continue
p_copy = p.copy()
p_copy['tags'] = valid_tags
products_in_stock.append(p_copy)
return render_template_string(POS_TEMPLATE, products_json=json.dumps(products_in_stock), settings=settings, currency_code=settings.get('currency_code', 'KGS'), env_id=env_id, emp_id=emp_id, repo_id=REPO_ID)
@app.route('//inventory')
def inventory_page(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('env_mode') != '2in1':
return "Остатки доступны только в режиме '2 в 1'", 403
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', [])
items =[]
low_stock_count = 0
for p in products:
for t in p.get('tags',[]):
stock = t.get('stock', 0)
is_low = stock <= 50
if is_low: low_stock_count += 1
items.append({
'product_id': p.get('product_id'),
'product_name': p.get('name'),
'tag_id': t.get('id'),
'tag_name': t.get('name'),
'stock': stock,
'price': t.get('price', 0),
'is_low': is_low
})
items.sort(key=lambda x: x['product_name'])
return render_template_string(
INVENTORY_TEMPLATE,
env_id=env_id, settings=settings, currency_code=settings.get('currency_code', 'KGS'),
items=items, low_stock_count=low_stock_count
)
@app.route('//inventory_action', methods=['POST'])
def inventory_action(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('env_mode') != '2in1':
return jsonify({"error": "Только для режима 2 в 1"}), 403
req = request.get_json()
p_id = req.get('product_id')
t_id = req.get('tag_id')
action = req.get('action')
qty = int(req.get('qty', 0))
if qty <= 0:
return jsonify({"error": "Количество должно быть больше 0"}), 400
products = data.get('products',[])
product = next((p for p in products if p.get('product_id') == p_id), None)
if not product: return jsonify({"error": "Товар не найден"}), 404
tag = next((t for t in product.get('tags',[]) if t.get('id') == t_id), None)
if not tag: return jsonify({"error": "Вариант не найден"}), 404
if 'stock_batches' not in tag:
tag['stock_batches'] =[{"qty": tag.get('stock', 0), "price": tag.get('price', 0), "box_price": tag.get('box_price', 0)}]
history_entry = {
'id': uuid4().hex,
'product_id': p_id,
'tag_id': t_id,
'type': action,
'qty': qty,
'timestamp': datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'),
'details': ''
}
if action == 'add':
new_price = req.get('new_price')
new_box_price = req.get('new_box_price')
if new_price is not None:
new_price = float(new_price)
new_box_price = float(new_box_price) if new_box_price else new_price * tag.get('box_qty', 1)
tag['stock_batches'].append({'qty': qty, 'price': new_price, 'box_price': new_box_price})
history_entry['details'] = f'Оприходование (новая цена: {new_price})'
else:
if tag['stock_batches']:
tag['stock_batches'][-1]['qty'] += qty
else:
tag['stock_batches'].append({'qty': qty, 'price': tag.get('price', 0), 'box_price': tag.get('box_price', 0)})
history_entry['details'] = 'Оприходование (старая цена)'
tag['stock'] = tag.get('stock', 0) + qty
update_tag_price_from_batches(tag)
elif action == 'write_off':
if tag.get('stock', 0) < qty:
return jsonify({"error": "Недостаточно остатков для списания"}), 400
remaining_to_deduct = qty
for batch in tag['stock_batches']:
if batch['qty'] > 0:
if batch['qty'] >= remaining_to_deduct:
batch['qty'] -= remaining_to_deduct
remaining_to_deduct = 0
break
else:
remaining_to_deduct -= batch['qty']
batch['qty'] = 0
tag['stock'] -= qty
history_entry['details'] = 'Ручное списание'
update_tag_price_from_batches(tag)
else:
return jsonify({"error": "Неизвестное действие"}), 400
if 'inventory_history' not in data:
data['inventory_history'] = []
data['inventory_history'].append(history_entry)
save_env_data(env_id, data)
return jsonify({"success": True})
@app.route('//inventory_history//')
def inventory_history(env_id, p_id, t_id):
data = get_env_data(env_id)
history = data.get('inventory_history',[])
item_history =[h for h in history if h.get('product_id') == p_id and h.get('tag_id') == t_id]
item_history.sort(key=lambda x: x['timestamp'], reverse=True)
return jsonify(item_history)
@app.route('//reports')
def reports_page(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
if settings.get('env_mode') != '2in1':
return "Отчеты доступны только в режиме '2 в 1'", 403
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))
now = datetime.now(ALMATY_TZ)
default_start = now.replace(day=1).strftime('%Y-%m-%d')
default_end = now.strftime('%Y-%m-%d')
start_date = request.args.get('start_date', default_start)
end_date = request.args.get('end_date', default_end)
orders = data.get('orders', {}).values()
filtered_orders =[]
for o in orders:
created_at = o.get('created_at', '')
if created_at:
date_part = created_at.split(' ')[0]
if start_date <= date_part <= end_date:
filtered_orders.append(o)
total_orders = len(filtered_orders)
total_revenue = sum(o.get('total_price', 0) for o in filtered_orders)
pos_orders = sum(1 for o in filtered_orders if o.get('source') == 'pos')
online_orders = total_orders - pos_orders
emp_stats = {}
product_sales = {}
for o in filtered_orders:
emp = o.get('employee_name') or 'Прямой заказ'
if emp not in emp_stats:
emp_stats[emp] = {'count': 0, 'revenue': 0}
emp_stats[emp]['count'] += 1
emp_stats[emp]['revenue'] += o.get('total_price', 0)
for item in o.get('cart',[]):
name = item.get('name', 'Неизвестно')
qty = item.get('quantity', 0)
if name not in product_sales:
product_sales[name] = 0
product_sales[name] += qty
top_products =[{'name': k, 'qty': v} for k, v in product_sales.items()]
top_products.sort(key=lambda x: x['qty'], reverse=True)
return render_template_string(
REPORTS_TEMPLATE, env_id=env_id, settings=settings, currency_code=settings.get('currency_code', 'KGS'),
total_orders=total_orders, total_revenue=total_revenue, pos_orders=pos_orders, online_orders=online_orders,
emp_stats=emp_stats, top_products=top_products[:20], start_date=start_date, end_date=end_date
)
@app.route('//track_view/', methods=['POST'])
def track_view(env_id, product_id):
data = get_env_data(env_id)
for p in data['products']:
if p.get('product_id') == product_id:
p['views'] = p.get('views', 0) + 1
break
save_env_data(env_id, data)
return jsonify({"status": "ok"})
@app.route('//product/')
def product_detail(env_id, product_id):
data = get_env_data(env_id)
all_products_raw = data.get('products',[])
settings = data.get('settings', {})
env_mode = settings.get('env_mode', 'external')
products_in_stock =[]
for p in all_products_raw:
if not p.get('in_stock', True):
continue
if env_mode == '2in1':
valid_tags =[t for t in p.get('tags', []) if t.get('stock', 0) > 0]
if not valid_tags and p.get('tags',[]):
continue
p_copy = p.copy()
p_copy['tags'] = valid_tags
products_in_stock.append(p_copy)
else:
products_in_stock.append(p)
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
product = next((p for p in products_sorted if p.get('product_id') == product_id), None)
if not product:
return "Товар не найден или отсутствует в наличии.", 404
return render_template_string(
PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID,
currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id
)
@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 or not order_data['cart']:
return jsonify({"error": "Корзина пуста или не передана."}), 400
data = get_env_data(env_id)
settings = data.get('settings', {})
products = data.get('products',[])
env_mode = settings.get('env_mode', 'external')
cart_items = order_data['cart']
customer_data = order_data.get('customer_data', {})
emp_id = order_data.get('emp_id')
source = order_data.get('source', 'catalog')
emp_name = None
emp_whatsapp = None
if emp_id:
employees = data.get('employees',[])
for emp in employees:
if emp.get('id') == emp_id:
emp_name = emp.get('name')
emp_whatsapp = emp.get('whatsapp')
break
total_price = 0
processed_cart =[]
order_timestamp = datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')
for item in cart_items:
if not all(k in item for k in ('name', 'quantity')):
return jsonify({"error": "Неверный формат товара в корзине."}), 400
try:
quantity = int(item['quantity'])
if quantity <= 0:
raise ValueError("Invalid quantity")
p_id = item.get('product_id')
c_color = item.get('color', 'N/A')
tx = item.get('tag_x')
ty = item.get('tag_y')
u_type = item.get('unit_type', 'piece')
product_ref = next((p for p in products if p.get('product_id') == p_id), None)
if not product_ref:
return jsonify({"error": f"Товар {p_id} не найден."}), 400
tag_ref = None
if 'TAG_' in c_color:
tag_id = c_color.split('_VAR_')[0].replace('TAG_', '')
tag_ref = next((t for t in product_ref.get('tags',[]) if t.get('id') == tag_id), None)
elif item.get('id') and len(item['id'].split('-')) >= 2:
tag_id = item['id'].split('-')[1]
tag_ref = next((t for t in product_ref.get('tags',[]) if t.get('id') == tag_id), None)
price = float(item.get('price', 0))
discount_applied = False
if tag_ref:
orig_price = float(tag_ref.get('price', 0))
box_price = float(tag_ref.get('box_price', orig_price))
box_qty = int(tag_ref.get('box_qty', 1))
if u_type == 'piece' and box_qty > 1 and quantity >= box_qty:
price = box_price / box_qty
discount_applied = True
elif u_type == 'box':
price = box_price
else:
price = orig_price
if env_mode == '2in1':
deduction = quantity
if u_type == 'box':
deduction = quantity * box_qty
if tag_ref.get('stock', 0) < deduction:
return jsonify({"error": f"Недостаточно остатков для товара {item['name']}."}), 400
if 'stock_batches' not in tag_ref:
tag_ref['stock_batches'] =[{"qty": tag_ref.get('stock', 0), "price": tag_ref.get('price', 0), "box_price": tag_ref.get('box_price', 0)}]
remaining_to_deduct = deduction
for batch in tag_ref['stock_batches']:
if batch['qty'] > 0:
if batch['qty'] >= remaining_to_deduct:
batch['qty'] -= remaining_to_deduct
remaining_to_deduct = 0
break
else:
remaining_to_deduct -= batch['qty']
batch['qty'] = 0
tag_ref['stock'] -= deduction
update_tag_price_from_batches(tag_ref)
if 'inventory_history' not in data:
data['inventory_history'] = []
data['inventory_history'].append({
'id': uuid4().hex,
'product_id': p_id,
'tag_id': tag_ref['id'],
'type': 'sale',
'qty': deduction,
'timestamp': order_timestamp,
'details': f"Продажа ({source})"
})
processed_cart.append({
"product_id": p_id, "name": item['name'], "price": price, "quantity": quantity,
"color": c_color, "photo": item.get('photo'), "tag_x": tx, "tag_y": ty, "unit_type": u_type,
"discount_applied": discount_applied,
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
})
total_price += price * quantity
except (ValueError, TypeError) as e:
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
order_id = f"{datetime.now(ALMATY_TZ).strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
new_order = {
"id": order_id, "created_at": order_timestamp, "cart": processed_cart,
"total_price": round(total_price, 2), "status": "new",
"employee_id": emp_id, "employee_name": emp_name, "employee_whatsapp": emp_whatsapp,
"customer_data": customer_data, "source": source
}
try:
if 'orders' not in data or not isinstance(data.get('orders'), dict):
data['orders'] = {}
data['orders'][order_id] = new_order
data['products'] = products
save_env_data(env_id, data)
return jsonify({"order_id": order_id}), 201
except Exception:
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
@app.route('//update_order/', methods=['POST'])
def update_order(env_id, order_id):
data = get_env_data(env_id)
order = data.get('orders', {}).get(order_id)
if not order:
return jsonify({"error": "Заказ не найден."}), 404
req = request.get_json()
idx = req.get('index')
action = req.get('action')
if idx is None or action not in['inc', 'dec', 'set', 'remove']:
return jsonify({"error": "Некорректный запрос."}), 400
try:
idx = int(idx)
cart = order.get('cart',[])
if idx < 0 or idx >= len(cart):
return jsonify({"error": "Товар не найден."}), 404
if action == 'inc':
cart[idx]['quantity'] += 1
elif action == 'dec':
cart[idx]['quantity'] -= 1
if cart[idx]['quantity'] <= 0:
cart.pop(idx)
elif action == 'set':
val = int(req.get('value', 1))
if val <= 0:
cart.pop(idx)
else:
cart[idx]['quantity'] = val
elif action == 'remove':
cart.pop(idx)
total = sum(float(item['price']) * int(item['quantity']) for item in cart)
order['total_price'] = round(total, 2)
order['cart'] = cart
save_env_data(env_id, data)
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('//delete_order/', methods=['POST'])
def delete_order(env_id, order_id):
data = get_env_data(env_id)
if 'orders' in data and order_id in data['orders']:
del data['orders'][order_id]
save_env_data(env_id, data)
flash("Заказ успешно удален.", "success")
else:
flash("Заказ не найден.", "error")
return redirect(url_for('history_page', env_id=env_id))
@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', {})
return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id)
@app.route('//history')
def history_page(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(data.get('orders', {}).values())
orders.sort(key=lambda x: x.get('created_at', ''), reverse=True)
employees = data.get('employees',[])
return render_template_string(HISTORY_TEMPLATE, orders=orders, employees=employees, settings=settings, currency_code=settings.get('currency_code', 'KGS'), env_id=env_id)
@app.route('//admin_ai_chat', methods=['POST'])
def admin_ai_chat(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 jsonify({"text": "Доступ запрещен."})
if not configure_gemini():
return jsonify({"text": "AI не настроен."})
req = request.get_json()
message = req.get('message')
history = req.get('history',[])
orders = data.get('orders', {})
products = data.get('products',[])
now = datetime.now(ALMATY_TZ)
current_month = now.strftime('%Y-%m')
monthly_revenue = 0
product_sales_counts = {}
for o in orders.values():
if o.get('created_at', '').startswith(current_month):
monthly_revenue += o.get('total_price', 0)
for item in o.get('cart',[]):
pid = item.get('product_id')
product_sales_counts[pid] = product_sales_counts.get(pid, 0) + item.get('quantity', 0)
sorted_views = sorted(products, key=lambda x: x.get('views', 0), reverse=True)[:5]
views_str_list = [f"[POST: {p['product_id']} Название: {p['name']}] (просмотров: {p.get('views', 0)})" for p in sorted_views if p.get('views', 0) > 0]
views_str = ", ".join(views_str_list) if views_str_list else "Нет просмотров"
sorted_sales_pids = sorted(product_sales_counts.items(), key=lambda x: x[1], reverse=True)[:5]
sales_str_list =[]
for pid, qty in sorted_sales_pids:
p = next((x for x in products if x['product_id'] == pid), None)
if p:
sales_str_list.append(f"[POST: {pid} Название: {p['name']}] (продано: {qty} шт)")
sales_str = ", ".join(sales_str_list) if sales_str_list else "Пока нет продаж"
currency = data['settings'].get('currency_code', 'KGS')
sys_prompt = f"""Ты — умный AI-ассистент администратора магазина.
Текущее время (Алматы): {now.strftime('%Y-%m-%d %H:%M:%S')}.
Выручка за этот месяц: {monthly_revenue} {currency}.
Самые просматриваемые товары (Топ-5): {views_str}.
Самые продаваемые товары (Топ-5): {sales_str}.
Если упоминаешь товар, используй точный формат:[POST: Название: ].
Помогай владельцу анализировать продажи и отвечать на бизнес-вопросы."""
try:
model = genai.GenerativeModel('gemma-4-31b-it')
messages =[{'role': 'user', 'parts':[{'text': sys_prompt}]}]
for h in history:
messages.append({'role': 'model' if h['role'] == 'ai' else 'user', 'parts':[{'text': h['text']}]})
chat = model.start_chat(history=messages)
resp = chat.send_message(message)
return jsonify({'text': resp.text})
except Exception as e:
return jsonify({'text': f"Ошибка AI: {str(e)}"})
@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',[])
organization_info = data.get('organization_info', {})
employees = data.get('employees',[])
blocks = data.get('blocks',[])
page = request.args.get('p', 1, type=int)
search_q = request.args.get('q', '').strip()
if 'orders' not in data or not isinstance(data.get('orders'), dict):
data['orders'] = {}
if request.method == 'POST':
action = request.form.get('action')
try:
if action == 'add_block':
b_type = request.form.get('block_type')
b_title = request.form.get('block_title', '').strip()
b_url = request.form.get('block_url', '').strip()
if b_url and not b_url.startswith(('http://', 'https://')):
b_url = 'https://' + b_url
b_content = request.form.get('block_content', '').strip()
blocks.append({
'id': uuid4().hex[:8],
'type': b_type,
'title': b_title,
'url': b_url,
'content': b_content
})
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Блок добавлен.", "success")
elif action == 'delete_block':
b_id = request.form.get('block_id')
data['blocks'] =[b for b in blocks if b.get('id') != b_id]
save_env_data(env_id, data)
flash("Блок удален.", "success")
elif action == 'move_block_up':
b_id = request.form.get('block_id')
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
if idx > 0:
blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx]
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Блок перемещен выше.", "success")
elif action == 'move_block_down':
b_id = request.form.get('block_id')
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
if idx != -1 and idx < len(blocks) - 1:
blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx]
data['blocks'] = blocks
save_env_data(env_id, data)
flash("Блок перемещен ниже.", "success")
elif action == 'add_employee':
emp_name = request.form.get('emp_name', '').strip()
emp_whatsapp = request.form.get('emp_whatsapp', '').strip()
if emp_name and emp_whatsapp:
emp_id = uuid4().hex[:8]
employees.append({'id': emp_id, 'name': emp_name, 'whatsapp': emp_whatsapp})
data['employees'] = employees
save_env_data(env_id, data)
flash("Сотрудник добавлен.", "success")
elif action == 'delete_employee':
emp_id = request.form.get('emp_id')
employees =[e for e in employees if e.get('id') != emp_id]
data['employees'] = employees
save_env_data(env_id, data)
flash("Сотрудник удален.", "success")
elif action == 'add_category':
category_name = request.form.get('category_name', '').strip()
if category_name and category_name not in categories:
categories.append(category_name)
data['categories'] = categories
save_env_data(env_id, data)
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
elif not category_name:
flash("Название категории не может быть пустым.", 'error')
else:
flash(f"Категория '{category_name}' уже существует.", 'error')
elif action == 'delete_category':
category_to_delete = request.form.get('category_name')
if category_to_delete and category_to_delete in categories:
categories.remove(category_to_delete)
updated_count = 0
for product in products:
if product.get('category') == category_to_delete:
product['category'] = 'Без категории'
updated_count += 1
data['categories'] = categories
data['products'] = products
save_env_data(env_id, data)
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
else:
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
elif action == 'update_org_info':
organization_info['about_us'] = request.form.get('about_us', '').strip()
organization_info['shipping'] = request.form.get('shipping', '').strip()
organization_info['returns'] = request.form.get('returns', '').strip()
organization_info['contact'] = request.form.get('contact', '').strip()
data['organization_info'] = organization_info
save_env_data(env_id, data)
flash("Информация о магазине успешно обновлена.", 'success')
elif action == 'update_settings':
settings['admin_password_enabled'] = 'admin_password_enabled' in request.form
settings['admin_password'] = request.form.get('admin_password', '').strip()
settings['organization_name'] = request.form.get('organization_name', 'Gippo312').strip()
settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip()
settings['currency_code'] = request.form.get('currency_code', 'KGS')
settings['business_type'] = request.form.get('business_type', 'retail')
settings['color_scheme'] = request.form.get('color_scheme', 'default')
settings['checkout_fields_enabled'] = 'checkout_fields_enabled' in request.form
settings['checkout_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
}
settings['categories_as_lines'] = 'categories_as_lines' in request.form
avatar_file = request.files.get('chat_avatar')
if avatar_file and avatar_file.filename:
if HF_TOKEN_WRITE:
try:
api = HfApi()
old_avatar = settings.get('chat_avatar')
if old_avatar:
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
ext = os.path.splitext(avatar_file.filename)[1].lower()
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
temp_path = os.path.join(uploads_dir, avatar_filename)
avatar_file.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
settings['chat_avatar'] = avatar_filename
os.remove(temp_path)
flash("Аватар успешно обновлен.", 'success')
except Exception as e:
flash(f"Ошибка при загрузке аватара: {e}", 'error')
else:
flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning")
data['settings'] = settings
save_env_data(env_id, data)
flash("Настройки успешно обновлены.", 'success')
elif action == 'add_product' or action == 'edit_product':
product_id = request.form.get('product_id')
product_data = {}
is_edit = action == 'edit_product'
if is_edit:
product_data = next((p for p in products if p.get('product_id') == product_id), None)
if not product_data:
flash(f"Ошибка: товар с ID {product_id} не найден.", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
else:
product_data['views'] = 0
product_data['name'] = request.form.get('name', '').strip()
product_data['description'] = request.form.get('description', '').strip()
category = request.form.get('category')
product_data['category'] = category if category in categories else 'Без категории'
tags_raw = request.form.get('tags_json', '[]')
try:
parsed_tags = json.loads(tags_raw)
for t in parsed_tags:
if 'stock_batches' not in t:
t['stock_batches'] =[{"qty": t.get('stock', 0), "price": t.get('price', 0), "box_price": t.get('box_price', 0)}]
product_data['tags'] = parsed_tags
except:
product_data['tags'] =[]
product_data['in_stock'] = 'in_stock' in request.form
product_data['is_top'] = 'is_top' in request.form
if not product_data['name']:
flash("Название товара обязательно.", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
photos_files = request.files.getlist('photos')
if photos_files and any(f.filename for f in photos_files):
if HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
new_photos_list =[]
photo_limit = 10
uploaded_count = 0
for photo in photos_files:
if uploaded_count >= photo_limit: break
if photo and photo.filename:
try:
ext = os.path.splitext(photo.filename)[1].lower()
if ext not in['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue
safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50]
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
temp_path = os.path.join(uploads_dir, photo_filename)
photo.save(temp_path)
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)
new_photos_list.append(photo_filename)
os.remove(temp_path)
uploaded_count += 1
except Exception as e:
flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error')
if new_photos_list and is_edit and product_data.get('photos'):
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
if new_photos_list:
product_data['photos'] = new_photos_list
else:
flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning")
if is_edit:
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
if product_index != -1:
products[product_index] = product_data
flash(f"Товар '{product_data['name']}' обновлен.", 'success')
else:
product_data['product_id'] = uuid4().hex
products.append(product_data)
flash(f"Товар '{product_data['name']}' добавлен.", 'success')
data['products'] = products
save_env_data(env_id, data)
elif action == 'delete_product':
product_id = request.form.get('product_id')
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
if product_index == -1:
flash(f"Ошибка удаления: товар не найден.", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
deleted_product = products.pop(product_index)
product_name = deleted_product.get('name', 'N/A')
photos_to_delete = deleted_product.get('photos',[])
if photos_to_delete and HF_TOKEN_WRITE:
try:
api = HfApi()
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
except Exception: pass
data['products'] = products
save_env_data(env_id, data)
flash(f"Товар '{product_name}' удален.", 'success')
else:
flash(f"Неизвестное действие: {action}", 'warning')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
except Exception as e:
flash(f"Ошибка при выполнении действия.", 'error')
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
filtered_products = products
if search_q:
q_lower = search_q.lower()
filtered_products =[p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()]
filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower())
PER_PAGE = 20
total_items = len(filtered_products)
total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1
if page < 1: page = 1
if page > total_pages: page = total_pages
start_idx = (page - 1) * PER_PAGE
end_idx = start_idx + PER_PAGE
paginated_products = filtered_products[start_idx:end_idx]
display_categories = sorted(categories)
display_organization_info = organization_info
display_settings = settings
chat_status = { "active": False, "expires_soon": False, "expires_date": "N/A" }
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
low_stock_count = 0
if settings.get('env_mode') == '2in1':
for p in products:
for t in p.get('tags',[]):
if t.get('stock', 0) <= 50:
low_stock_count += 1
return render_template_string(
ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, search_q=search_q, categories=display_categories,
organization_info=display_organization_info, chats={}, settings=display_settings, employees=employees,
blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url,
currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, env_id=env_id, chat_status=chat_status, low_stock_count=low_stock_count
)
@app.route('/generate_description_ai', methods=['POST'])
def handle_generate_description_ai():
request_data = request.get_json()
base64_image = request_data.get('image')
language = request_data.get('language', 'Русский')
if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400
try:
image_data = base64.b64decode(base64_image)
result_text = generate_ai_description_from_image(image_data, language)
return jsonify({"text": result_text})
except ValueError as ve: return jsonify({"error": str(ve)}), 400
except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
if __name__ == '__main__':
configure_gemini()
download_db_from_hf()
load_data()
if HF_TOKEN_WRITE:
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
backup_thread.start()
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)