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 %}
Активные среды
{% for env in active_envs %}
{% endfor %}
Архив сред
{% for env in archived_envs %}
{% endfor %}
'''
CATALOG_TEMPLATE = '''
{{ settings.organization_name }}
{% if mode == 'pos' %}
{{ t('Режим кассы: Быстрое оформление') }}
{% endif %}
{% if settings.catalog_subtitle %}
{{ settings.catalog_subtitle }}
{% endif %}
{% 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 }}
{{ t('Корзина') }}
{{ t('Оформить заказ') }}
'''
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 %}
{% 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 %}
{{ 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 %}
{% 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 %}
{% 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) }}
{% endif %}
{% endfor %}
Общее кол-во: {{ 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 %}
{% endif %}
{% if settings.qr_code_2_url %}
{% 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 %}
{{ 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) }}
{% endif %}
{% endfor %}
Итог ({{ 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' %}
WhatsApp
{% else %}
{% if messenger == 'tg' %}
Telegram
{% else %}
WhatsApp
{% endif %}
{% endif %}
'''
ASSEMBLY_TEMPLATE = '''
Сборка №{{ order.id }}
{% 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 }}
Номер
Дата
Клиент / Сотрудник
Сумма ({{ 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') %}
{% if pending_orders %}
{% for order in pending_orders %}
Клиент: {{ order.customer_name }} ({{ order.customer_phone }})
{% if order.staff_name %}
Сотрудник: {{ order.staff_name }}
{% endif %}
Сумма: {{ order.total_price }} {{ currency_code }}
Скидка для заказа №{{ order.id }}
{% endfor %}
{% else %}
Нет новых онлайн заказов
{% endif %}
{% if sys_mode != 'internal' %}
{% for u in catalog_users %}
{{ u.name }}
Пароль: {{ u.password }}
{% endfor %}
{% endif %}
{% if sys_mode != 'light_external' %}
{% for s in staff %}
{{ s.name }} ({{ s.whatsapp }})
{% if sys_mode != 'internal' %}
Ссылка (Онлайн)
{% endif %}
{% if sys_mode != 'external' %}
Ссылка (Касса)
{% endif %}
{% endfor %}
{% endif %}
Импорт и Экспорт товаров
Экспорт
Скачать текущую базу товаров в формате JSON.
Скачать JSON
Сброс данных:
Сбросить историю продаж
Управление категориями
Добавить
{% 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 %}
{% if category_photos.get(category) %}
{% endif %}
Сохранить категорию
Добавить товар
Новый товар в категории "{{ category }}"
{% if settings.business_type == 'wholesale' %}
Оптовые цены (от кол-ва)
Добавить оптовую цену
{% endif %}
Описание товара
Фотографии товара (до 10 шт)
Сохранить товар
{% 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.business_type == 'wholesale' %}
Оптовые цены (от кол-ва)
Добавить оптовую цену
{% endif %}
Варианты товара
Разные цены
{% for variant in product.variants %}
{% endfor %}
Добавить вариант
Описание товара
{{ product.description }}
Сохранить изменения
{% 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 %}
{% 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%
Маржинальность по товарам
Товар
Кол-во (шт)
Выручка
Себест.
Прибыль
Маржа %
{% endif %}
'''
INVENTORY_TEMPLATE = '''
Учет остатков
Текущие остатки
Заканчивающиеся {{ low_stock_items|length }}
Заказы {{ active_purchases|length }}
История операций
Товар
Вариант
Категория
Остаток
{% for item in low_stock_items %}
{{ item.name }}
{{ item.variant }}
{{ item.category }}
{{ item.stock }}
{% else %}
Нет заканчивающихся товаров
{% endfor %}
Дата и время
Товар
Ожидается (шт)
Действия
{% for po in active_purchases %}
{{ po.date }}
{{ po.product_name }} {% if po.variant_name %}{{ po.variant_name }} {% endif %}
{{ po.qty }}
{% else %}
Нет активных заказов поставщикам
{% endfor %}
Дата
Товар
Тип
Кол-во
Комментарий
{% for h in history|reverse %}
{{ h.date }}
{{ h.product }} {% if h.variant %}({{ h.variant }}){% endif %}
{% if h.change > 0 %}
Приход
{% else %}
Списание
{% endif %}
{{ h.change|abs }}
{{ h.comment }}
{% else %}
История пуста
{% endfor %}
'''
DEBTS_TEMPLATE = '''
Долги клиентов
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
Активные долги
История долгов
Дата
Клиент
Накладная
Действие
Сумма
{% for h in debt_history %}
{{ h.date }}
{{ h.customer }}
{{ h.order_id }}
{{ h.action }}
{{ h.amount }} {{ currency }}
{% else %}
История пуста
{% endfor %}
'''
PURCHASES_TEMPLATE = '''
Заказы поставщикам
Товар
Категория
Текущий остаток
Действие
{% for p in products %}
{% if not p.variants %}
{{ p.name }}
ID: {{ p.product_id }}
{{ p.category }}
{{ p.stock if p.stock != "" else "0" }}
Сделать заказ
{% else %}
{% for v in p.variants %}
{{ p.name }} ({{ v.name }})
ID: {{ p.product_id }}
{{ p.category }}
{{ v.stock if v.stock != "" else "0" }}
Сделать заказ
{% endfor %}
{% endif %}
{% endfor %}
'''
@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)