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 import math from PIL import Image, ImageOps from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, session, Response 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() TRANSLATIONS = { 'ru': {}, 'kk': { 'Каталог': 'Каталог', 'Поиск товаров...': 'Тауарларды іздеу...', 'Поиск': 'Іздеу', 'Ничего не найдено': 'Ештеңе табылмады', 'В этой категории пока нет товаров': 'Бұл санатта әзірге тауарлар жоқ', 'В упаковке:': 'Қаптамада:', 'В коробке:': 'Қорапта:', 'Мин. заказ:': 'Мин. тапсырыс:', 'Нет в наличии': 'Қолжетімсіз', 'Остаток:': 'Қалдық:', 'От': 'Бастап', 'шт': 'дана', 'кор.': 'қор.', 'уп.': 'қап.', '+ Упаковка': '+ Қаптама', '+ Коробка': '+ Қорап', 'Сумма заказа:': 'Тапсырыс сомасы:', 'Корзина': 'Себет', 'Ваш заказ': 'Сіздің тапсырысыңыз', 'Имя клиента (необязательно)': 'Клиенттің аты (міндетті емес)', 'Ваше Имя': 'Сіздің атыңыз', 'Номер телефона': 'Телефон нөмірі', 'Город': 'Қала', 'Адрес доставки': 'Жеткізу мекенжайы', 'Индекс': 'Индекс', 'Оформить заказ': 'Тапсырыс беру', 'Оформление...': 'Рәсімдеу...', 'Админ-панель': 'Әкімші тақтасы', 'Отчеты': 'Есептер', 'Остатки': 'Қалдықтар', 'История накладных и заказов': 'Жүкқұжаттар мен тапсырыстар тарихы', 'Сборка': 'Құрастыру', 'В каталог': 'Каталогқа', 'Выход': 'Шығу', 'Сохранить на сервер': 'Серверге сақтау', 'Скачать с сервера': 'Серверден жүктеу', 'Онлайн заказы': 'Онлайн тапсырыстар', 'Пользователи (Закрытый каталог)': 'Пайдаланушылар (Жабық каталог)', 'Персонал': 'Қызметкерлер', 'Настройка магазина': 'Дүкен параметрлері', 'Управление категориями': 'Санаттарды басқару', 'Добавить товар': 'Тауар қосу', 'Название товара': 'Тауар атауы', 'Наличие': 'Болуы', 'Штрих-код': 'Штрих-код', 'Цена за ед.': 'Бір данасының бағасы', 'В уп. (шт)': 'Қаптамада (дана)', 'В кор. (шт)': 'Қорапта (дана)', 'Цена за упаковку': 'Қаптама бағасы', 'Мин. заказ (шт)': 'Мин. тапсырыс (дана)', 'Остаток на складе': 'Қоймадағы қалдық', 'Описание товара': 'Тауар сипаттамасы', 'Фотографии товара (до 10 шт)': 'Тауар фотосуреттері (10 дейін)', 'Сохранить товар': 'Тауарды сақтау', 'Да': 'Иә', 'Нет': 'Жоқ', 'Варианты товара': 'Тауар нұсқалары', 'Язык интерфейса': 'Интерфейс тілі', 'Учет коробок': 'Қораптар есебі', 'Включить учет больших коробок': 'Үлкен қораптар есебін қосу', 'Общая скидка на чек (сумма)': 'Чектегі жалпы жеңілдік (сома)', 'WhatsApp клиента (напр. +77001234567) необязательно': 'Клиенттің WhatsApp (мыс. +77001234567) міндетті емес', 'Режим кассы: Быстрое оформление': 'Касса режимі: Тез рәсімдеу', 'Сохраненные кассы': 'Сақталған кассалар', 'Новая касса': 'Жаңа касса', 'Свой товар': 'Өз тауарыңыз' }, 'uz': { 'Каталог': 'Katalog', 'Поиск товаров...': 'Tovarlarni qidirish...', 'Поиск': 'Qidirish', 'Ничего не найдено': 'Hech narsa topilmadi', 'В этой категории пока нет товаров': 'Ushbu toifada hozircha tovarlar yoʻq', 'В упаковке:': 'Oʻramda:', 'В коробке:': 'Qutida:', 'Мин. заказ:': 'Min. buyurtma:', 'Нет в наличии': 'Mavjud emas', 'Остаток:': 'Qoldiq:', 'От': 'Dan', 'шт': 'dona', 'кор.': 'quti', 'уп.': 'oʻram', '+ Упаковка': '+ Oʻram', '+ Коробка': '+ Quti', 'Сумма заказа:': 'Buyurtma summasi:', 'Корзина': 'Savat', 'Ваш заказ': 'Sizning buyurtmangiz', 'Имя клиента (необязательно)': 'Mijoz ismi (majburiy emas)', 'Ваше Имя': 'Ismingiz', 'Номер телефона': 'Telefon raqami', 'Город': 'Shahar', 'Адрес доставки': 'Yetkazib berish manzili', 'Индекс': 'Indeks', 'Оформить заказ': 'Buyurtma berish', 'Оформление...': 'Rasmiylashtirilmoqda...', 'Админ-панель': 'Admin panel', 'Отчеты': 'Hisobotlar', 'Остатки': 'Qoldiqlar', 'История накладных и заказов': 'Yukxat va buyurtmalar tarixi', 'Сборка': 'Yigʻish', 'В каталог': 'Katalogga', 'Выход': 'Chiqish', 'Сохранить на сервер': 'Serverga saqlash', 'Скачать с сервера': 'Serverdan yuklab olish', 'Онлайн заказы': 'Onlayn buyurtmalar', 'Пользователи (Закрытый каталог)': 'Foydalanuvchilar (Yopiq katalog)', 'Персонал': 'Xodimlar', 'Настройка магазина': 'Do\'kon sozlamalari', 'Управление категориями': 'Toifalarni boshqarish', 'Добавить товар': 'Tovar qo\'shish', 'Название товара': 'Tovar nomi', 'Наличие': 'Mavjudligi', 'Штрих-код': 'Shtrix-kod', 'Цена за ед.': 'Dona narxi', 'В уп. (шт)': 'O\'ramda (dona)', 'В кор. (шт)': 'Qutida (dona)', 'Цена за упаковку': 'O\'ram narxi', 'Мин. заказ (шт)': 'Min. buyurtma (dona)', 'Остаток на складе': 'Ombordagi qoldiq', 'Описание товара': 'Tovar tavsifi', 'Фотографии товара (до 10 шт)': 'Tovar rasmlari (10 tagacha)', 'Сохранить товар': 'Tovarni saqlash', 'Да': 'Ha', 'Нет': 'Yo\'q', 'Варианты товара': 'Tovar variantlari', 'Язык интерфейса': 'Interfeys tili', 'Учет коробок': 'Qutilar hisobi', 'Включить учет больших коробок': 'Katta qutilar hisobini yoqish', 'Общая скидка на чек (сумма)': 'Chek uchun umumiy chegirma (summa)', 'WhatsApp клиента (напр. +77001234567) необязательно': 'Mijoz WhatsApp (mas. +77001234567) majburiy emas', 'Режим кассы: Быстрое оформление': 'Kassa rejimi: Tez rasmiylashtirish', 'Сохраненные кассы': 'Saqlangan kassalar', 'Новая касса': 'Yangi kassa', 'Свой товар': 'O\'z tovaringiz' } } def get_t(lang='ru'): def t(text): if not isinstance(text, str): return text return TRANSLATIONS.get(lang, {}).get(text, text) return t 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':[], 'purchase_orders': [], 'debt_history': [], 'settings': { 'organization_name': 'Default Shop', 'admin_password_enabled': False, 'admin_password': '', 'logo_url': DEFAULT_LOGO_URL, 'qr_code_1_url': '', 'qr_code_2_url': '', 'catalog_subtitle': '', 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER, 'order_messenger': 'wa', 'order_contact': DEFAULT_WHATSAPP_NUMBER, 'invoice_contacts': '', 'currency': '₸', 'track_inventory': False, 'use_barcodes': False, 'use_master_boxes': False, 'margin_tracking': False, 'language': 'ru', 'business_type': 'mixed', 'system_mode': 'both', 'hide_stock_online': False, 'closed_catalog_enabled': False, 'commission_enabled': False, 'commission_percent': 0.0, 'theme': 'light', 'is_deleted': False, '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'}, 'kaspi': {'enabled': False, 'url': ''} } } } } 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 'purchase_orders' not in env_data: env_data['purchase_orders'] = []; changed = True if 'debt_history' not in env_data: env_data['debt_history'] = []; changed = True 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 'qr_code_1_url' not in settings: settings['qr_code_1_url'] = ''; changed = True if 'qr_code_2_url' not in settings: settings['qr_code_2_url'] = ''; changed = True if 'catalog_subtitle' not in settings: settings['catalog_subtitle'] = ''; changed = True if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True if 'order_messenger' not in settings: settings['order_messenger'] = 'wa'; changed = True if 'order_contact' not in settings: settings['order_contact'] = settings.get('whatsapp_number', DEFAULT_WHATSAPP_NUMBER); changed = True if 'invoice_contacts' not in settings: settings['invoice_contacts'] = ''; changed = True if 'is_deleted' not in settings: settings['is_deleted'] = False; changed = True if 'currency' not in settings: settings['currency'] = '₸'; changed = True elif settings['currency'] == 'T': settings['currency'] = '₸'; 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 'use_master_boxes' not in settings: settings['use_master_boxes'] = False; changed = True if 'margin_tracking' not in settings: settings['margin_tracking'] = False; changed = True if 'language' not in settings: settings['language'] = 'ru'; 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 'commission_enabled' not in settings: settings['commission_enabled'] = False; changed = True if 'commission_percent' not in settings: settings['commission_percent'] = 0.0; 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'}, 'kaspi': {'enabled': False, 'url': ''} } changed = True else: if 'kaspi' not in settings['socials']: settings['socials']['kaspi'] = {'enabled': False, 'url': ''} changed = True for product in env_data['products']: if 'created_at' not in product: product['created_at'] = '2000-01-01 00:00:00'; changed = True 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 'pieces_per_master_box' not in product: product['pieces_per_master_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 'cost_price' not in product: product['cost_price'] = ""; 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 'cost_price' not in v: v['cost_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 'pieces_per_master_box' not in v: v['pieces_per_master_box'] = product.get('pieces_per_master_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 if 'photo' not in v: v['photo'] = ""; 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 if 'commission_amount' not in order: order['commission_amount'] = 0; changed = True if 'is_debt' not in order: order['is_debt'] = False; changed = True if 'paid_amount' not in order: order['paid_amount'] = order.get('total_price', 0); changed = True if 'debt_amount' not in order: order['debt_amount'] = 0; changed = True for item in order.get('cart', []): if 'discount' not in item: item['discount'] = 0; changed = True if 'cost_price' not in item: item['cost_price'] = 0; changed = True if 'category' not in item: item['category'] = 'Без категории'; changed = True if 'wholesale_tiers' not in item: item['wholesale_tiers'] = []; changed = True if 'pieces_per_master_box' not in item: item['pieces_per_master_box'] = 1; 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':[], 'purchase_orders': [], 'debt_history': [], 'settings': { 'organization_name': f'Shop {env_id}', 'admin_password_enabled': False, 'admin_password': '', 'logo_url': DEFAULT_LOGO_URL, 'qr_code_1_url': '', 'qr_code_2_url': '', 'catalog_subtitle': '', 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER if 'DEFAULT_WHATSAPP_NUMBER' in globals() else "+77470623684", 'order_messenger': 'wa', 'order_contact': DEFAULT_WHATSAPP_NUMBER if 'DEFAULT_WHATSAPP_NUMBER' in globals() else "+77470623684", 'invoice_contacts': '', 'currency': '₸', 'track_inventory': False, 'use_barcodes': False, 'use_master_boxes': False, 'margin_tracking': False, 'language': 'ru', 'business_type': 'mixed', 'system_mode': 'both', 'hide_stock_online': False, 'closed_catalog_enabled': False, 'commission_enabled': False, 'commission_percent': 0.0, 'theme': 'light', 'is_deleted': False, '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': ''}, 'kaspi': {'enabled': False, '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) @app.before_request def check_deleted_env(): if request.endpoint and request.view_args and 'env_id' in request.view_args: env_id = request.view_args['env_id'] if env_id in ['admhosto', 'default_env']: return all_data = load_data() if env_id in all_data and all_data[env_id].get('settings', {}).get('is_deleted', False): return "

Среда отключена

", 403 def update_order_totals(order, settings): business_type = settings.get('business_type', 'mixed') commission_enabled = settings.get('commission_enabled', False) commission_percent = float(settings.get('commission_percent', 0.0)) 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) if commission_enabled and commission_percent > 0: commission_amount = total * (commission_percent / 100.0) order['commission_amount'] = round(commission_amount, 2) total += commission_amount else: order['commission_amount'] = 0 order['total_price'] = round(total, 2) if order.get('is_debt', False): order['debt_amount'] = round(order['total_price'] - float(order.get('paid_amount', 0)), 2) if order['debt_amount'] <= 0: order['is_debt'] = False order['debt_amount'] = 0 order['paid_amount'] = order['total_price'] else: order['paid_amount'] = order['total_price'] order['debt_amount'] = 0 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: if item.get('is_custom', False): continue 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' %}
{{ t('Режим кассы: Быстрое оформление') }}
{% endif %}
{% if settings.catalog_subtitle %}
{{ settings.catalog_subtitle }}
{% endif %}

{{ settings.organization_name }}

{% if mode == 'pos' %}
1
{% if staff_id %} {% endif %} {% 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 %} {% if settings.currency == '₸' and settings.socials.kaspi.enabled and settings.socials.kaspi.url %} {% for link in settings.socials.kaspi.url.split() %} {% if link.strip() %} {% endif %} {% endfor %} {% endif %} {% endif %}
{{ t('Сумма заказа:') }} 0 {{ currency_code }}
''' ORDER_TEMPLATE = ''' Накладная №{{ order.id }}
{% set total_qty = order.cart | selectattr('quantity', '>', 0) | sum(attribute='quantity') %}
{{ 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 %}
{% if order.is_debt %}
В долг: {{ order.debt_amount }} {{ currency_code }}
{% endif %}
{% for item in order.cart %} {% set ppb = item.pieces_per_box|default(1)|int %} {% set ppmb = item.pieces_per_master_box|default(1)|int %} {% 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.use_master_boxes and ppmb > 1 and item.quantity >= ppmb %} {% set master = item.quantity // ppmb %} {% set rem_master = item.quantity % ppmb %} {% set packs = (rem_master // ppb) if ppb > 1 else 0 %} {% set rem = (rem_master % ppb) if ppb > 1 else rem_master %} {{ master }} {{ t('кор.') }}{% if packs > 0 %} {{ packs }} {{ t('уп.') }}{% endif %}{% if rem > 0 %} {{ rem }} {{ t('шт') }}{% endif %} {% elif settings.business_type != 'retail' and ppb > 1 and (item.quantity // ppb) > 0 %} {% set packs = item.quantity // ppb %} {% set rem = item.quantity % ppb %} {{ packs }} {{ t('уп.') }}{% if rem > 0 %} {{ rem }} {{ t('шт') }}{% endif %} {% else %} {{ item.quantity }} {{ t('шт') }} {% endif %}
{{ item.calculated_price | round(2) }} {{ (item.calculated_price * item.quantity) | round(2) }}
Общее кол-во: {{ total_qty }} шт
{% if order.global_discount > 0 %}
Применена общая скидка: -{{ order.global_discount }} {{ currency_code }}
{% endif %} {% if settings.commission_enabled and order.commission_amount and order.commission_amount > 0 %}
Комиссия ({{ settings.commission_percent }}%): +{{ order.commission_amount }} {{ currency_code }}
{% endif %} Итого: {{ order.total_price }} {{ currency_code }}
{% if settings.qr_code_1_url %} QR Code 1 {% endif %} {% if settings.qr_code_2_url %} QR Code 2 {% endif %}
{{ 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 %} {% else %}
Покупатель: {{ order.customer_name if order.customer_name else 'Розничный покупатель' }}
{% if order.customer_whatsapp %}
WA: {{ order.customer_whatsapp }}
{% endif %} {% endif %} {% if order.staff_name %}
Кассир: {{ order.staff_name }}
{% endif %}
{% for item in order.cart %} {% if item.quantity > 0 %} {% endif %} {% endfor %}
Товар Кол Сумма
{{ item.name }}{% if item.variant_name %} ({{ item.variant_name }}){% endif %}
{{ item.calculated_price | round(2) }} {{ currency_code }}
{{ item.quantity }} {{ (item.calculated_price * item.quantity) | round(2) }}
Итог ({{ total_qty }} шт): {{ order.total_price }} {{ currency_code }}
{% if order.global_discount > 0 %}
Скидка: -{{ order.global_discount }} {{ currency_code }}
{% endif %} {% if settings.commission_enabled and order.commission_amount and order.commission_amount > 0 %}
Комиссия: +{{ order.commission_amount }} {{ currency_code }}
{% endif %}
К ОПЛАТЕ: {{ order.total_price }} {{ currency_code }}
{% if order.is_debt %}
Оплачено: {{ order.paid_amount }} {{ currency_code }}
В ДОЛГ: {{ order.debt_amount }} {{ currency_code }}
{% endif %}
{% if settings.qr_code_1_url %} {% endif %} {% if settings.qr_code_2_url %} {% endif %}
*** СПАСИБО ЗА ПОКУПКУ! ***
{% set messenger = settings.order_messenger|default('wa') %} {% if order.status == 'pos' %} {% else %} {% if messenger == 'tg' %} {% else %} {% endif %} {% endif %}
''' 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 = ''' {{ t('Админ-панель') }} {% macro render_pagination(current, total, param_name, extra_args) %} {% if total > 1 %} {% endif %} {% endmacro %}
{% set sys_mode = settings.system_mode|default('both') %}

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

{% if sys_mode != 'light_external' %} {{ t('Отчеты') }} {% endif %} {% if sys_mode not in ['external', 'light_external'] %} {{ t('Остатки') }} {% if low_stock_count > 0 %}{{ low_stock_count }}{% endif %} {% if sys_mode == 'both' %} Заказы поставщикам {% endif %} Долги {% endif %} {% if sys_mode == 'both' %} {{ t('История накладных и заказов') }} {% elif sys_mode in ['external', 'light_external'] %} {{ t('История заказов') }} {% else %} {{ t('История накладных') }} {% endif %} {% if sys_mode != 'light_external' %} {{ t('Сборка') }} {% if unassembled_count > 0 %}{{ unassembled_count }}{% endif %} {% endif %} {% if sys_mode != 'internal' %} {{ t('В каталог') }} {% endif %} {% if settings.admin_password_enabled %} {{ t('Выход') }} {% endif %}
{{ t('Онлайн заказы') }} {% 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' %}
{{ t('Пользователи (Закрытый каталог)') }}
{% for u in catalog_users %}
{{ u.name }} Пароль: {{ u.password }}
{% endfor %}
{% endif %} {% if sys_mode != 'light_external' %}
{{ t('Персонал') }}
{% for s in staff %}
{{ s.name }} ({{ s.whatsapp }})
{% if sys_mode != 'internal' %} {% endif %} {% if sys_mode != 'external' %} {% endif %}
{% endfor %}
{% endif %}

Импорт и Экспорт товаров

Экспорт

Скачать текущую базу товаров в формате JSON.

Скачать JSON

Импорт

Загрузите JSON файл с товарами. Показать образец

{{ t('Настройка магазина') }}
Текущий логотип:
{% if settings.qr_code_1_url %}
Текущий QR 1:
{% endif %}
{% if settings.qr_code_2_url %}
Текущий QR 2:
{% endif %}
Сброс данных:

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

{% set search_q = request.args.get('q', '').lower() %} {% set cat_page = request.args.get('page', 1)|int %} {% set per_page = 10 %}
{% set filtered_cats = [] %} {% if search_q %} {% for c in categories %} {% set ns_cat = namespace(match=false) %} {% if search_q in c|lower %} {% set ns_cat.match = true %} {% else %} {% for p in products %} {% if p.category == c %} {% if search_q in p.name|lower or search_q in p.product_id|lower or (p.barcode and search_q in p.barcode|lower) %} {% set ns_cat.match = true %} {% else %} {% for v in p.variants %} {% if v.barcode and search_q in v.barcode|lower %} {% set ns_cat.match = true %} {% endif %} {% endfor %} {% endif %} {% endif %} {% endfor %} {% endif %} {% if ns_cat.match %} {% set _ = filtered_cats.append(c) %} {% endif %} {% endfor %} {% else %} {% set filtered_cats = categories %} {% endif %} {% set total_cats = filtered_cats|length %} {% set total_pages = (total_cats / per_page)|round(0, 'ceil')|int %} {% if total_pages == 0 %}{% set total_pages = 1 %}{% endif %} {% set start_idx = (cat_page - 1) * per_page %} {% set end_idx = start_idx + per_page %} {% set paged_cats = filtered_cats[start_idx:end_idx] %} {% for category in paged_cats %}
{{ category }}
{% if category_photos.get(category) %}
{% endif %}
Добавить товар
Новый товар в категории "{{ category }}"
{% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.margin_tracking and sys_mode == 'both' %}
{% endif %} {% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% 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 %}
{% set ns = namespace(count=0) %} {% for product in products %} {% if product.category == category %} {% set ns_prod = namespace(match=false) %} {% if not search_q %} {% set ns_prod.match = true %} {% else %} {% if search_q in product.name|lower or search_q in product.product_id|lower or search_q in category|lower or (product.barcode and search_q in product.barcode|lower) %} {% set ns_prod.match = true %} {% else %} {% for v in product.variants %} {% if v.barcode and search_q in v.barcode|lower %} {% set ns_prod.match = true %} {% endif %} {% endfor %} {% endif %} {% endif %} {% if ns_prod.match %} {% if ns.count < 50 or search_q %}
{% 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 %} {% if settings.margin_tracking and sys_mode == 'both' and product.cost_price %} | Себестоимость: {{ product.cost_price }} {{ currency_code }} {% endif %} {% endif %} {% if settings.business_type != 'retail' %} • В уп: {{ product.pieces_per_box|default(1) }} шт {% if settings.use_master_boxes and product.pieces_per_master_box %} • В кор: {{ product.pieces_per_master_box }} шт {% endif %} {% 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.margin_tracking and sys_mode == 'both' %}
{% endif %} {% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% 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 variant.photo %}
{% endif %}
{% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% endif %} {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
{% endif %}
{% if settings.margin_tracking and sys_mode == 'both' %}
{% 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 %} {% set ns.count = ns.count + 1 %} {% endif %} {% endif %} {% endfor %} {% if ns.count > 50 and not search_q %}
Показано 50 из {{ ns.count }} товаров. Воспользуйтесь поиском, чтобы найти остальные.
{% endif %}
{% endfor %} {{ render_pagination(cat_page, total_pages, 'page', '&q=' ~ search_q) }}
''' REPORTS_TEMPLATE = ''' Отчеты

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

Назад в панель
Период:
Общий отчет
По дням
По категориям
По сотрудникам
{% if settings.margin_tracking and settings.system_mode == 'both' %}
Маржа и Прибыль
{% endif %}
Общая выручка
0
Кол-во заказов
0
Средний чек (AOV)
0
Продано товаров (шт)
0
Возвраты (сумма)
0
{% if settings.commission_enabled %}
Сумма комиссий ({{ settings.commission_percent }}%)
0
{% endif %}

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

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

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

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

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

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

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

Сотрудник Кол-во заказов Выручка ({{ currency_code }})
{% if settings.margin_tracking and settings.system_mode == 'both' %}
Чистая прибыль
0
Себестоимость проданного
0
Средняя маржинальность
0%

Маржинальность по товарам

Товар Кол-во (шт) Выручка Себест. Прибыль Маржа %
{% endif %}
''' INVENTORY_TEMPLATE = ''' Учет остатков

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

Назад в панель
Текущие остатки
Заканчивающиеся {{ low_stock_items|length }}
Заказы {{ active_purchases|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 po in active_purchases %} {% else %} {% endfor %}
Дата и время Товар Ожидается (шт) Действия
{{ po.date }} {{ po.product_name }} {% if po.variant_name %}
{{ po.variant_name }}{% endif %}
{{ po.qty }}
Нет активных заказов поставщикам
{% 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 }}
История пуста
''' DEBTS_TEMPLATE = ''' Долги клиентов

Учет долгов

Назад в панель
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
Активные долги
История долгов
{% for o in debts %} {% else %} {% endfor %}
Накладная Дата Клиент Общая сумма Оплачено Долг Внести оплату
{{ o.id }} {{ o.created_at.split(' ')[0] }} {{ o.customer_name if o.customer_name else 'Касса' }}
{{ o.customer_whatsapp }}
{{ o.total_price }} {{ currency }} {{ o.paid_amount }} {{ currency }} {{ o.debt_amount }} {{ currency }}
Нет активных долгов
{% for h in debt_history %} {% else %} {% endfor %}
Дата Клиент Накладная Действие Сумма
{{ h.date }} {{ h.customer }} {{ h.order_id }} {{ h.action }} {{ h.amount }} {{ currency }}
История пуста
''' PURCHASES_TEMPLATE = ''' Заказы поставщикам

Заказы поставщикам

В остатки В панель
{% 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" }}
''' @app.route('/') def index(): return render_template_string(LANDING_PAGE_TEMPLATE) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() active_envs = [] archived_envs = [] 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}") env_info = { "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") } if settings.get('is_deleted', False): archived_envs.append(env_info) else: active_envs.append(env_info) active_envs.sort(key=lambda x: x['id']) archived_envs.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, active_envs=active_envs, archived_envs=archived_envs) @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':[], 'purchase_orders': [], 'debt_history': [], 'settings': { "organization_name": f"Shop {new_id}", "admin_password_enabled": False, "admin_password": "", "logo_url": DEFAULT_LOGO_URL, "qr_code_1_url": "", "qr_code_2_url": "", "catalog_subtitle": "", "whatsapp_number": DEFAULT_WHATSAPP_NUMBER, "order_messenger": "wa", "order_contact": DEFAULT_WHATSAPP_NUMBER, "invoice_contacts": "", "currency": "₸", "track_inventory": False, "use_barcodes": False, "use_master_boxes": False, "margin_tracking": False, "language": "ru", "business_type": "mixed", "system_mode": "both", "hide_stock_online": False, "closed_catalog_enabled": False, "commission_enabled": False, "commission_percent": 0.0, "theme": "light", "is_deleted": False, "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': ''}, 'kaspi': {'enabled': False, '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'] =[] all_data[env_id]['debt_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: all_data[env_id]['settings']['is_deleted'] = True save_data(all_data) flash(f'Среда {env_id} была отключена (в архиве).', 'success') else: flash(f'Среда {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/restore/', methods=['POST']) def restore_environment(env_id): all_data = load_data() if env_id in all_data: all_data[env_id]['settings']['is_deleted'] = False save_data(all_data) flash(f'Среда {env_id} восстановлена.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/hard_delete/', methods=['POST']) def hard_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') 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', {}) t = get_t(settings.get('language', 'ru')) 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', '₸'), settings=settings, env_id=env_id, mode=mode, staff_id=staff_id, t=t ) @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/upload_custom_photo', methods=['POST']) def upload_custom_photo(env_id): file = request.files.get('photo') if file and file.filename: filename = process_and_upload_image(file, 'photos', size=(512, 512)) if filename: return jsonify({'success': True, 'filename': filename}) return jsonify({'success': False}) @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 and not item.get('is_custom'): 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']) if order.get('is_debt'): order['debt_amount'] = round(order['total_price'] - float(order.get('paid_amount', 0)), 2) if order['debt_amount'] <= 0: order['is_debt'] = False order['debt_amount'] = 0 order['paid_amount'] = order['total_price'] 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 and not item.get('is_custom'): restore_stock(c_key, item.get('product_id'), item.get('variant_idx', -1), ret_qty, data['products']) break update_order_totals(order, data['settings']) if order['total_price'] <= 0: order['status'] = 'returned' order['is_debt'] = False order['debt_amount'] = 0 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 for p in data.get('products', [])} processed_cart =[] for item in cart_items: if item.get('is_custom'): photo_url = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+" if item.get('photo_custom'): photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo_custom']}" 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": 0, "cost_price": 0, "quantity": int(item['quantity']), "pieces_per_box": 1, "pieces_per_master_box": 1, "variant_name": '', "variant_idx": -1, "discount": float(item.get('discount', 0)), "wholesale_tiers": [], "category": 'Свой товар', "photo_url": photo_url, "is_custom": True }) continue p_info = product_dict.get(item.get('product_id'), {}) cat = p_info.get('category', 'Без категории') photo_url = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+" vidx = item.get('variant_idx', -1) cost_price = p_info.get('cost_price', 0) if vidx != -1 and p_info.get('variants') and len(p_info['variants']) > vidx: v_photo = p_info['variants'][vidx].get('photo') if v_photo: photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{v_photo}" elif item.get('photos'): photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" cost_price = p_info['variants'][vidx].get('cost_price', cost_price) elif item.get('photos'): photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" 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)), "cost_price": float(cost_price) if str(cost_price).strip() else 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, "pieces_per_master_box": int(item.get('pieces_per_master_box', 1)) if str(item.get('pieces_per_master_box', 1)).strip() != "" else 1, "variant_name": item.get('variant_name', ''), "variant_idx": vidx, "discount": float(item.get('discount', 0)), "wholesale_tiers": item.get('wholesale_tiers', []), "category": cat, "photo_url": photo_url, "is_custom": False }) 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": {}, "commission_amount": 0, "is_debt": order_data.get('is_debt', False), "paid_amount": float(order_data.get('paid_amount', 0)), "debt_amount": 0 } update_order_totals(new_order, data['settings']) 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']) if new_order['is_debt'] and new_order['debt_amount'] > 0: if 'debt_history' not in data: data['debt_history'] = [] data['debt_history'].append({ 'id': uuid4().hex, 'date': get_almaty_time(), 'order_id': order_id, 'customer': customer_name or 'Касса', 'action': 'Возникновение долга', 'amount': new_order['debt_amount'] }) 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', {}) t = get_t(settings.get('language', 'ru')) if not order: return "Order not found", 404 return render_template_string( ORDER_TEMPLATE, order=order, settings=settings, currency_code=settings.get('currency', '₸'), env_id=env_id, t=t ) @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']) 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']) 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('//pay_debt/', methods=['POST']) def pay_debt(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) if order and order.get('is_debt'): amount = float(request.form.get('amount', 0)) if amount > 0: order['paid_amount'] = round(float(order.get('paid_amount', 0)) + amount, 2) order['debt_amount'] = round(float(order.get('total_price', 0)) - order['paid_amount'], 2) if order['debt_amount'] <= 0: order['is_debt'] = False order['debt_amount'] = 0 order['paid_amount'] = order.get('total_price', 0) if 'debt_history' not in data: data['debt_history'] = [] data['debt_history'].append({ 'id': uuid4().hex, 'date': get_almaty_time(), 'order_id': order_id, 'customer': order.get('customer_name') or 'Касса', 'action': 'Оплата долга', 'amount': amount }) save_env_data(env_id, data) flash(f'Оплата в размере {amount} успешно внесена для заказа №{order_id}', 'success') return redirect(url_for('debts', env_id=env_id)) @app.route('//debts') def debts(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)) all_orders = list(data.get('orders', {}).values()) debt_orders = [o for o in all_orders if o.get('is_debt', False) and float(o.get('debt_amount', 0)) > 0] debt_orders.sort(key=lambda x: x.get('created_at', ''), reverse=True) history_list = data.get('debt_history', []) history_list = sorted(history_list, key=lambda x: x.get('date', ''), reverse=True) return render_template_string( DEBTS_TEMPLATE, env_id=env_id, debts=debt_orders, debt_history=history_list, currency=settings.get('currency', '₸') ) @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', '₸'), 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', '₸'), 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, settings=settings, currency_code=settings.get('currency', '₸'), 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',[])) active_purchases = data.get('purchase_orders', []) 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, active_purchases=active_purchases) @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('//purchases') def purchases(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') != 'both': return redirect(url_for('admin', env_id=env_id)) return render_template_string(PURCHASES_TEMPLATE, env_id=env_id, products=data.get('products', [])) @app.route('//api/purchase', methods=['POST']) def create_purchase(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)) if qty <= 0: return jsonify({"success": False}) name = "" v_name = "" for p in data['products']: if p['product_id'] == pid: name = p['name'] if vidx != -1 and vidx < len(p.get('variants', [])): v_name = p['variants'][vidx]['name'] break po_id = uuid4().hex data['purchase_orders'].append({ 'id': po_id, 'date': get_almaty_time(), 'product_id': pid, 'variant_idx': vidx, 'product_name': name, 'variant_name': v_name, 'qty': qty }) save_env_data(env_id, data) return jsonify({"success": True}) @app.route('//api/receive_purchase', methods=['POST']) def receive_purchase(env_id): data = get_env_data(env_id) req = request.json po_id = req.get('po_id') qty_received = int(req.get('qty', 0)) if qty_received <= 0: return jsonify({"success": False}) po = next((x for x in data['purchase_orders'] if x['id'] == po_id), None) if not po: return jsonify({"success": False}) pid = po['product_id'] vidx = po['variant_idx'] for p in data['products']: if p['product_id'] == pid: if vidx != -1 and vidx < len(p.get('variants', [])): 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_received 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_received data['inventory_history'].append({ "date": get_almaty_time(), "product": p['name'], "variant": po['variant_name'], "change": qty_received, "comment": "Поступление от поставщика" }) break if qty_received >= po['qty']: data['purchase_orders'] = [x for x in data['purchase_orders'] if x['id'] != po_id] else: po['qty'] -= qty_received save_env_data(env_id, data) return jsonify({"success": True}) @app.route('//export_products', methods=['GET']) def export_products(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', []) export_data = [] for p in products: export_data.append({ "product_id": p.get("product_id"), "name": p.get("name", ""), "price": p.get("price", ""), "cost_price": p.get("cost_price", ""), "category": p.get("category", ""), "stock": p.get("stock", ""), "barcode": p.get("barcode", ""), "description": p.get("description", ""), "is_available": p.get("is_available", True), "pieces_per_box": p.get("pieces_per_box", ""), "pieces_per_master_box": p.get("pieces_per_master_box", ""), "box_price": p.get("box_price", ""), "min_order": p.get("min_order", ""), "has_variant_prices": p.get("has_variant_prices", False), "variants": p.get("variants", []) }) json_str = json.dumps(export_data, ensure_ascii=False, indent=2) return Response( json_str, mimetype='application/json', headers={'Content-disposition': f'attachment; filename=products_{env_id}.json'} ) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) t = get_t(settings.get('language', 'ru')) 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'] =[] data['debt_history'] = [] save_env_data(env_id, data) flash('История продаж и заказов успешно очищена.', 'success') elif action == 'import_products': file = request.files.get('import_file') mode = request.form.get('import_mode', 'append') if file and file.filename.endswith('.json'): try: imported_products = json.load(file) if not isinstance(imported_products, list): raise ValueError("JSON должен содержать список товаров") if mode == 'replace': data['products'] = [] data['categories'] = [] for p in imported_products: if not p.get('name') or not p.get('category'): continue cat_name = str(p['category']).strip() if cat_name not in data['categories']: data['categories'].append(cat_name) existing_p = None if p.get('product_id'): existing_p = next((x for x in data['products'] if x['product_id'] == p['product_id']), None) if existing_p and mode == 'append': existing_p.update({ 'name': str(p.get('name', '')), 'price': p.get('price', ''), 'cost_price': p.get('cost_price', ''), 'category': cat_name, 'stock': p.get('stock', ''), 'barcode': str(p.get('barcode', '')), 'description': str(p.get('description', '')), 'is_available': p.get('is_available', True), 'pieces_per_box': p.get('pieces_per_box', ''), 'pieces_per_master_box': p.get('pieces_per_master_box', ''), 'box_price': p.get('box_price', ''), 'min_order': p.get('min_order', ''), 'has_variant_prices': p.get('has_variant_prices', False), 'variants': p.get('variants', []) }) else: new_p = { 'product_id': p.get('product_id') or uuid4().hex, 'created_at': get_almaty_time(), 'name': str(p.get('name', '')), 'price': p.get('price', ''), 'cost_price': p.get('cost_price', ''), 'category': cat_name, 'stock': p.get('stock', ''), 'barcode': str(p.get('barcode', '')), 'description': str(p.get('description', '')), 'is_available': p.get('is_available', True), 'pieces_per_box': p.get('pieces_per_box', ''), 'pieces_per_master_box': p.get('pieces_per_master_box', ''), 'box_price': p.get('box_price', ''), 'min_order': p.get('min_order', ''), 'has_variant_prices': p.get('has_variant_prices', False), 'variants': p.get('variants', []), 'photos': p.get('photos', []), 'wholesale_tiers': p.get('wholesale_tiers', []) } data['products'].append(new_p) save_env_data(env_id, data) flash('Импорт товаров успешно завершен!', 'success') except Exception as e: flash(f'Ошибка импорта: {str(e)}', 'error') else: flash('Пожалуйста, загрузите валидный JSON файл.', 'error') elif action == 'update_settings': settings['language'] = request.form.get('language', 'ru') settings['organization_name'] = request.form.get('organization_name', '').strip() settings['catalog_subtitle'] = request.form.get('catalog_subtitle', '').strip() settings['theme'] = request.form.get('theme', 'light') settings['business_type'] = request.form.get('business_type', 'mixed') settings['whatsapp_number'] = request.form.get('order_contact', '').strip() settings['order_messenger'] = request.form.get('order_messenger', 'wa') settings['order_contact'] = request.form.get('order_contact', '').strip() settings['invoice_contacts'] = request.form.get('invoice_contacts', '').strip() settings['currency'] = request.form.get('currency', '₸') sys_m = settings.get('system_mode', 'both') if sys_m == 'external': settings['track_inventory'] = False settings['use_barcodes'] = False settings['use_master_boxes'] = False settings['margin_tracking'] = False settings['hide_stock_online'] = False settings['closed_catalog_enabled'] = 'closed_catalog_enabled' in request.form elif sys_m == 'light_external': settings['track_inventory'] = False settings['use_barcodes'] = False settings['use_master_boxes'] = 'use_master_boxes' in request.form settings['margin_tracking'] = 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['use_master_boxes'] = 'use_master_boxes' in request.form settings['margin_tracking'] = 'margin_tracking' 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['commission_enabled'] = 'commission_enabled' in request.form try: settings['commission_percent'] = float(request.form.get('commission_percent', 0.0)) except ValueError: settings['commission_percent'] = 0.0 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 } def process_image_setting(field_name, dict_key, folder="logos"): img_file = request.files.get(field_name) if img_file and img_file.filename and HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) ext = os.path.splitext(img_file.filename)[1].lower() if ext in ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.svg']: img_filename = f"{field_name}_{uuid4().hex}{ext}" temp_path = os.path.join(uploads_dir, img_filename) img_file.save(temp_path) try: api = HfApi() api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"{folder}/{img_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) settings[dict_key] = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{folder}/{img_filename}" except Exception: pass finally: if os.path.exists(temp_path): os.remove(temp_path) process_image_setting('logo', 'logo_url') process_image_setting('qr_code_1', 'qr_code_1_url') process_image_setting('qr_code_2', 'qr_code_2_url') 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() settings['socials']['kaspi']['enabled'] = 'kaspi_enabled' in request.form settings['socials']['kaspi']['url'] = request.form.get('kaspi_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 "" cp_str = request.form.get('cost_price', '') cost_price = float(cp_str) if cp_str else "" ppb_str = request.form.get('pieces_per_box', '') pieces_per_box = int(ppb_str) if ppb_str else "" ppmb_str = request.form.get('pieces_per_master_box', '') pieces_per_master_box = int(ppmb_str) if ppmb_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)}) var_indices = request.form.getlist('variant_indices[]') variants = [] for idx in var_indices: v_name = request.form.get(f'variant_name_{idx}', '').strip() if v_name: v_price = price v_cost_price = cost_price v_box_price = box_price v_is_avail = request.form.get(f'variant_is_available_{idx}', '1') == '1' v_wholesale_tiers = [] if has_variant_prices: vp = request.form.get(f'variant_price_{idx}') if vp: v_price = float(vp) vcp = request.form.get(f'variant_cost_price_{idx}') if vcp: v_cost_price = float(vcp) vbp = request.form.get(f'variant_box_price_{idx}') if vbp: v_box_price = float(vbp) vt_qtys = request.form.getlist(f'variant_{idx}_tier_qty[]') vt_prices = request.form.getlist(f'variant_{idx}_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 = request.form.get(f'variant_stock_{idx}', '') if v_stock: v_stock = int(v_stock) v_barcode = request.form.get(f'variant_barcode_{idx}', '').strip() v_ppb = pieces_per_box vppb_val = request.form.get(f'variant_pieces_per_box_{idx}') if vppb_val: v_ppb = int(vppb_val) v_ppmb = pieces_per_master_box vppmb_val = request.form.get(f'variant_pieces_per_master_box_{idx}') if vppmb_val: v_ppmb = int(vppmb_val) v_photo = "" photo_file = request.files.get(f'variant_photo_{idx}') if photo_file and photo_file.filename: filename = process_and_upload_image(photo_file, 'photos', size=(512, 512)) if filename: v_photo = filename variants.append({ "name": v_name, "barcode": v_barcode, "price": v_price, "cost_price": v_cost_price, "box_price": v_box_price, "stock": v_stock, "pieces_per_box": v_ppb, "pieces_per_master_box": v_ppmb, "is_available": v_is_avail, "wholesale_tiers": v_wholesale_tiers, "photo": v_photo }) 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, 'created_at': get_almaty_time(), 'name': name, 'barcode': barcode, 'price': price, 'cost_price': cost_price, 'pieces_per_box': pieces_per_box, 'pieces_per_master_box': pieces_per_master_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') new_cat = request.form.get('new_category', '').strip() 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 "" cp_str = request.form.get('cost_price', '') cost_price = float(cp_str) if cp_str else "" ppb_str = request.form.get('pieces_per_box', '') pieces_per_box = int(ppb_str) if ppb_str else "" ppmb_str = request.form.get('pieces_per_master_box', '') pieces_per_master_box = int(ppmb_str) if ppmb_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[]') var_indices = request.form.getlist('variant_indices[]') variants = [] for idx in var_indices: v_name = request.form.get(f'variant_name_{idx}', '').strip() if v_name: v_price = price v_cost_price = cost_price v_box_price = box_price v_is_avail = request.form.get(f'variant_is_available_{idx}', '1') == '1' v_wholesale_tiers = [] if has_variant_prices: vp = request.form.get(f'variant_price_{idx}') if vp: v_price = float(vp) vcp = request.form.get(f'variant_cost_price_{idx}') if vcp: v_cost_price = float(vcp) vbp = request.form.get(f'variant_box_price_{idx}') if vbp: v_box_price = float(vbp) vt_qtys = request.form.getlist(f'variant_{idx}_tier_qty[]') vt_prices = request.form.getlist(f'variant_{idx}_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 = request.form.get(f'variant_stock_{idx}', '') if v_stock: v_stock = int(v_stock) v_barcode = request.form.get(f'variant_barcode_{idx}', '').strip() v_ppb = pieces_per_box vppb_val = request.form.get(f'variant_pieces_per_box_{idx}') if vppb_val: v_ppb = int(vppb_val) v_ppmb = pieces_per_master_box vppmb_val = request.form.get(f'variant_pieces_per_master_box_{idx}') if vppmb_val: v_ppmb = int(vppmb_val) v_photo = request.form.get(f'variant_existing_photo_{idx}', '') if request.form.get(f'variant_remove_photo_{idx}') == '1': v_photo = '' photo_file = request.files.get(f'variant_photo_{idx}') if photo_file and photo_file.filename: filename = process_and_upload_image(photo_file, 'photos', size=(512, 512)) if filename: v_photo = filename variants.append({ "name": v_name, "barcode": v_barcode, "price": v_price, "cost_price": v_cost_price, "box_price": v_box_price, "stock": v_stock, "pieces_per_box": v_ppb, "pieces_per_master_box": v_ppmb, "is_available": v_is_avail, "wholesale_tiers": v_wholesale_tiers, "photo": v_photo }) 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 if new_cat and new_cat in categories: p['category'] = new_cat p['barcode'] = barcode p['price'] = price p['cost_price'] = cost_price p['pieces_per_box'] = pieces_per_box p['pieces_per_master_box'] = pieces_per_master_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', '₸'), 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, t=t ) @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)