import os
import base64
import json
import threading
import time
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
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/fullpl")
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
DEFAULT_WHATSAPP_NUMBER = "+48123456789"
DEFAULT_LOGO_URL = "https://huggingface.co/spaces/Metapp/Tech/resolve/main/1776929812446-019db944-b5db-7524-8f44-73942d70a0f8.png"
data_lock = threading.Lock()
TRANSLATIONS = {
'pl': {
'Каталог': 'Katalog', 'Поиск товаров...': 'Szukaj produktów...', 'Поиск': 'Szukaj',
'Ничего не найдено': 'Nic nie znaleziono', 'В этой категории пока нет товаров': 'Brak produktów w tej kategorii',
'В упаковке:': 'W opakowaniu:', 'В коробке:': 'W kartonie:', 'Мин. заказ:': 'Min. zamówienie:',
'Нет в наличии': 'Brak na stanie', 'Остаток:': 'Stan:', 'От': 'Od', 'шт': 'szt',
'кор.': 'kart.', 'уп.': 'opak.', '+ Упаковка': '+ Opakowanie', '+ Коробка': '+ Karton',
'Сумма заказа:': 'Kwota zamówienia:', 'Корзина': 'Koszyk', 'Ваш заказ': 'Twoje zamówienie',
'Имя клиента (необязательно)': 'Imię klienta (opcjonalnie)', 'Ваше Имя': 'Twoje imię',
'Номер телефона': 'Numer telefonu', 'Город': 'Miasto', 'Адрес доставки': 'Adres dostawy',
'Индекс': 'Kod pocztowy', 'Оформить заказ': 'Złóż zamówienie', 'Оформление...': 'Przetwarzanie...',
'Админ-панель': 'Panel administratora', 'Отчеты': 'Raporty', 'Остатки': 'Stany magazynowe',
'История накладных и заказов': 'Historia faktur i zamówień', 'Сборка': 'Kompletacja',
'В каталог': 'Do katalogu', 'Выход': 'Wyloguj', 'Сохранить на сервер': 'Zapisz na serwer',
'Скачать с сервера': 'Pobierz z serwera', 'Онлайн заказы': 'Zamówienia online',
'Пользователи (Закрытый каталог)': 'Użytkownicy (Katalog zamknięty)', 'Персонал': 'Personel',
'Настройка магазина': 'Ustawienia sklepu', 'Управление категориями': 'Zarządzanie kategoriami',
'Добавить товар': 'Dodaj produkt', 'Название товара': 'Nazwa produktu', 'Наличие': 'Dostępność',
'Штрих-код': 'Kod kreskowy', 'Цена за ед.': 'Cena za szt.', 'В уп.': 'W opak.',
'В кор.': 'W kart.', 'Цена за упаковку': 'Cena za opakowanie', 'Мин. заказ': 'Min. zamówienie',
'Остаток на складе': 'Stan na magazynie', 'Описание товара': 'Opis produktu',
'Фотографии товара (до 10 шт)': 'Zdjęcia produktu (do 10 szt)', 'Сохранить товар': 'Zapisz produkt',
'Да': 'Tak', 'Нет': 'Nie', 'Варианты товара': 'Warianty produktu', 'Язык интерфейса': 'Język interfejsu',
'Учет коробок': 'Ewidencja kartonów', 'Включить учет больших коробок': 'Włącz ewidencję dużych kartonów',
'Общая скидка на чек (сумма)': 'Całkowita zniżka na paragon (kwota)',
'WhatsApp клиента (напр. +77001234567) необязательно': 'WhatsApp klienta (opcjonalnie)',
'Режим кассы: Быстрое оформление': 'Tryb kasy: Szybka obsługa', 'Сохраненные кассы': 'Zapisane kasy',
'Новая касса': 'Nowa kasa', 'Свой товар': 'Własny produkt', 'Заказы поставщикам': 'Zamówienia u dostawców',
'Долги': 'Zadłużenia', 'История заказов': 'Historia zamówień', 'Текущие остатки': 'Bieżące stany',
'Заканчивающиеся': 'Kończące się', 'История операций': 'Historia operacji', 'Общее количество товаров': 'Całkowita liczba produktów',
'Общая стоимость склада': 'Całkowita wartość magazynu', 'ед.': 'szt.', 'Сделать заказ': 'Złóż zamówienie',
'Приход': 'Przychód', 'Списание': 'Rozchód', 'Товар': 'Produkt', 'Категория': 'Kategoria', 'Действие': 'Akcja', 'Загрузка...': 'Ładowanie...',
'пачка': 'paczka', 'кв.м': 'mkw', 'пог. м': 'mb', 'кг': 'kg', 'л': 'l', 'Назад в панель': 'Powrót do panelu',
'Подтвержден': 'Potwierdzone', 'Касса': 'Kasa', 'Возврат': 'Zwrot', 'Ожидает': 'Oczekuje', 'Касса / Онлайн': 'Kasa / Online',
'Накладная': 'Faktura', 'Вы уверены, что хотите удалить накладную? Товары вернутся на склад и удалятся из отчетов.': 'Czy na pewno chcesz usunąć fakturę? Produkty wrócą na magazyn.',
'Поиск по номеру, клиенту или сотруднику...': 'Szukaj po numerze, kliencie lub pracowniku...', 'Период:': 'Okres:', 'Сбросить': 'Resetuj',
'Номер': 'Numer', 'Дата': 'Data', 'Клиент / Сотрудник': 'Klient / Pracownik', 'Сумма': 'Kwota', 'Долг': 'Dług', 'Статус': 'Status', 'Действия': 'Akcje',
'История накладных': 'Historia faktur', 'Сборка накладной': 'Kompletacja faktury', 'Мои продажи (Возврат)': 'Moja sprzedaż (Zwrot)',
'Мои накладные': 'Moje faktury', 'Мои продажи': 'Moja sprzedaż', 'Возвраты': 'Zwroty',
'Store Name': 'Nazwa sklepu', 'Subtitle': 'Podtytuł', 'Business Type': 'Typ działalności',
'Retail': 'Detal', 'Retail + Wholesale': 'Detal + Hurt', 'Wholesale': 'Hurt',
'Currency': 'Waluta', 'Polski złoty (zł)': 'Polski złoty (zł)', 'Euro (€)': 'Euro (€)', 'US Dollar ($)': 'Dolar amerykański ($)', 'Українська гривня (₴)': 'Hrywna ukraińska (₴)',
'Order Messenger': 'Komunikator zamówień', 'Order Contact': 'Kontakt ds. zamówień',
'Invoice Contacts': 'Kontakty na fakturze', 'Logo': 'Logo', 'QR code 1': 'Kod QR 1', 'QR code 2': 'Kod QR 2',
'Customer Fields (Checkout)': 'Pola klienta (Kasa)', 'Name': 'Imię', 'Phone': 'Telefon', 'City': 'Miasto', 'Address': 'Adres', 'Zip': 'Kod pocztowy',
'Inventory': 'Magazyn', 'Large Box Tracking': 'Śledzenie dużych kartonów', 'Track stock': 'Śledź stan magazynowy', 'Barcodes': 'Kody kreskowe',
'Profit Tracking (Cost Price)': 'Śledzenie zysków (Cena zakupu)', 'Hide stock online': 'Ukryj stan magazynowy online',
'Catalog Access': 'Dostęp do katalogu', 'Password-protected catalog': 'Katalog chroniony hasłem',
'Commission Fee': 'Prowizja', 'Enable Fee': 'Włącz prowizję', 'Percent (e.g. 0.95)': 'Procent (np. 0.95)',
'Security': 'Bezpieczeństwo', 'Admin Password': 'Hasło administratora', 'Set Password': 'Ustaw hasło',
'Social Links': 'Linki społecznościowe', 'Save Settings': 'Zapisz ustawienia',
'Wipe Data': 'Wyczyść dane', 'Clear Order History': 'Wyczyść historię zamówień',
'UX/UI Design': 'Wygląd UX/UI', 'Theme': 'Motyw', 'Light': 'Jasny', 'Dark': 'Ciemny', 'Magma': 'Magma', 'Ocean': 'Ocean', 'Forest': 'Las', 'Cyberpunk': 'Cyberpunk',
'Primary Color (HEX)': 'Kolor główny (HEX)', 'Secondary Color (HEX)': 'Kolor dodatkowy (HEX)',
'Font': 'Czcionka', 'Header Style': 'Styl nagłówka', 'Glassmorphism': 'Szkło', 'Solid': 'Jednolity', 'Gradient': 'Gradient',
'Card Style': 'Styl karty', 'Shadow': 'Cień', 'Flat': 'Płaski', 'Bordered': 'Z obramowaniem',
'Border Radius': 'Zaokrąglenie rogów', 'Small': 'Małe', 'Medium': 'Średnie', 'Large': 'Duże', 'Max': 'Maksymalne',
'Shadow Intensity': 'Intensywność cienia', 'Image Aspect Ratio': 'Proporcje obrazu', 'Square (1:1)': 'Kwadrat (1:1)', 'Classic (4:3)': 'Klasyczny (4:3)', 'Portrait (3:4)': 'Portret (3:4)', 'Wide (16:9)': 'Szeroki (16:9)',
'Animation Speed': 'Szybkość animacji', 'Fast': 'Szybko', 'Normal': 'Normalnie', 'Smooth': 'Płynnie',
'Button Shape': 'Kształt przycisku', 'Rounded': 'Zaokrąglone', 'Pill': 'Pigułka', 'Square': 'Kwadrat',
'List View for Categories': 'Widok listy dla kategorii', 'Compact Mode': 'Tryb kompaktowy', 'Save Design': 'Zapisz wygląd',
'Category Name': 'Nazwa kategorii', 'Category Photo (512x512)': 'Zdjęcie kategorii (512x512)', 'Add': 'Dodaj',
'Delete Category?': 'Usunąć kategorię?', 'Edit Product': 'Edytuj produkt', 'Status': 'Status', 'Available': 'Dostępny', 'Out of Stock': 'Brak na stanie',
'Price': 'Cena', 'Cost Price': 'Cena zakupu', 'In Pack': 'W opakowaniu', 'In Box': 'W kartonie', 'Pack Price': 'Cena za opak.', 'Stock': 'Stan magazynowy',
'Wholesale Prices': 'Ceny hurtowe', 'Add Tier': 'Dodaj poziom', 'Variants': 'Warianty', 'Variant Prices': 'Ceny wariantów', 'Add Variant': 'Dodaj wariant',
'Description': 'Opis', 'Photos (up to 10)': 'Zdjęcia (do 10)', 'Remove': 'Usuń', 'Save Product': 'Zapisz produkt', 'Delete product?': 'Usunąć produkt?',
'Showing 50 of': 'Wyświetlanie 50 z', 'Cancel': 'Anuluj', 'Current Month': 'Bieżący miesiąc', 'General': 'Ogólne', 'Daily': 'Dziennie', 'By Category': 'Według kategorii', 'By Staff': 'Według pracownika', 'Profit & Margin': 'Zysk i marża', 'Total Revenue': 'Całkowity przychód', 'Orders Count': 'Liczba zamówień', 'Avg Order Value': 'Średnia wartość zamówienia', 'Items Sold': 'Sprzedane przedmioty', 'Returns': 'Zwroty', 'Commissions': 'Prowizje', 'Top Products': 'Najlepsze produkty', 'Qty': 'Ilość', 'Sum': 'Suma', 'Daily Sales': 'Sprzedaż dzienna', 'Orders': 'Zamówienia', 'Revenue': 'Przychód', 'Net Profit': 'Zysk netto', 'COGS (Cost)': 'Koszty własne', 'Avg Margin': 'Średnia marża', 'Profit by Product': 'Zysk wg produktu', 'Rev': 'Przychód', 'Cost': 'Koszt', 'Profit': 'Zysk', 'Margin %': 'Marża %', 'Import / Export': 'Import / Eksport', 'Export': 'Eksport', 'Download JSON DB.': 'Pobierz bazę JSON', 'Download JSON': 'Pobierz JSON', 'Import': 'Import', 'Upload JSON DB.': 'Prześlij bazę JSON', 'Sample': 'Próbka', 'Append / Update': 'Dołącz / Aktualizuj', 'Replace all': 'Zastąp wszystko', 'Upload': 'Prześlij', 'W złote': 'W złote', 'Link (Online)': 'Link (Online)', 'Link (POS)': 'Link (POS)', 'Client': 'Klient', 'Staff': 'Personel', 'Invoice': 'Faktura', 'Discount': 'Rabat', 'Confirm': 'Potwierdź', 'Delete': 'Usuń',
'Вход в панель': 'Logowanie do panelu', 'Введите пароль': 'Wprowadź hasło', 'Войти': 'Zaloguj', 'Закрытый каталог': 'Katalog zamknięty', 'Введите 6-значный пароль для доступа': 'Wprowadź 6-cyfrowe hasło', 'Пароль': 'Hasło', 'Управление': 'Zarządzanie', 'Среды': 'Środowiska', 'Создать новую среду': 'Utwórz nowe środowisko', 'Поиск...': 'Szukaj...', 'Активные среды': 'Aktywne środowiska', 'Сброс': 'Resetuj', 'Архив сред': 'Archiwum środowisk', 'Восстановить': 'Przywróć', 'Окончательно': 'Trwale usunąć', 'Сохранить': 'Zapisz', 'Отмена': 'Anuluj', 'Добавить долг': 'Dodaj dług', 'Имя клиента': 'Imię klienta', 'Сумма долга': 'Kwota długu', 'Нет активных долгов': 'Brak aktywnych długów', 'Оплатить': 'Opłać', 'История пуста': 'Historia jest pusta', 'Оплата долга': 'Spłata długu', 'Возникновение долга': 'Powstanie długu', 'Нет данных': 'Brak danych', 'Печать': 'Drukuj', 'Кол': 'Ilość', 'Итог': 'Razem', 'Скидка': 'Rabat', 'Комиссия': 'Prowizja', 'К ОПЛАТЕ': 'DO ZAPŁATY', 'Оплачено': 'Zapłacono', 'В ДОЛГ': 'ZALEGŁOŚĆ', 'СПАСИБО ЗА ПОКУПКУ!': 'DZIĘKUJEMY ZA ZAKUPY!', 'ТОВАРНЫЙ ЧЕК': 'PARAGON', 'Покупатель': 'Kupujący', 'Тел': 'Tel', 'Кассир': 'Kasjer', 'Pic': 'Zdj', 'Expected': 'Oczekiwane', 'Full': 'Całość', 'Partial': 'Część', 'Type': 'Typ', 'Comment': 'Komentarz', 'Receipt from supplier': 'Odbiór od dostawcy', 'Активные долги': 'Aktywne długi', 'История долгов': 'Historia długów', 'Накладные': 'Faktury', 'Посл. Дата': 'Ostatnia Data', 'Общая сумма': 'Całkowita kwota', 'Внести оплату': 'Wpłać należność', 'Наличные': 'Gotówka', 'Безналичные': 'Karta/Przelew', 'New Product in': 'Nowy produkt w', 'Color, size': 'Kolor, rozmiar', 'Per unit': 'Za sztukę', 'Opt': 'Hurt', 'Edit': 'Edytuj', 'Save': 'Zapisz', 'Add Product': 'Dodaj produkt', 'No online orders': 'Brak zamówień online', 'Admin (Manual)': 'Admin (Ręcznie)', 'Manual debt added': 'Ręcznie dodany dług', 'Group Payment': 'Płatność grupowa', 'Debt payment': 'Spłata długu', 'В остатки': 'Do magazynu', 'Заказ товара': 'Zamówienie produktu', 'Количество': 'Ilość', 'Подтвердить': 'Potwierdź', 'Сборка накладной №': 'Kompletacja faktury nr', 'Нужно:': 'Wymagane:', 'Закончить сборку': 'Zakończ kompletację', 'Завершить сборку?': 'Zakończyć kompletację?', 'Ссылка скопирована!': 'Skopiowano link!', 'Скопировать ссылку': 'Skopiuj link', 'Назад к списку': 'Powrót do listy', 'Общее кол-во': 'Łączna ilość', 'Применена общая скидка': 'Zastosowano rabat ogólny', 'Итого': 'Razem', 'Вариант:': 'Wariant:', '2 в 1': '2 w 1', 'Внутренний учет': 'Księgowość wew.', 'Внешний учет': 'Księgowość zewn.', 'Лайт внешка': 'Zewn. (Light)', 'Админ': 'Admin', 'Processing...': 'Przetwarzanie...', 'Adding...': 'Dodawanie...', 'Error': 'Błąd', 'Link copied!': 'Link skopiowany!', 'Delete order?': 'Usunąć zamówienie?'
},
'de': {
'Каталог': 'Katalog', 'Поиск товаров...': 'Produkte suchen...', 'Поиск': 'Suche',
'Ничего не найдено': 'Nichts gefunden', 'В этой категории пока нет товаров': 'In dieser Kategorie gibt es noch keine Produkte',
'В упаковке:': 'In Verpackung:', 'В коробке:': 'Im Karton:', 'Мин. заказ:': 'Min. Bestellung:',
'Нет в наличии': 'Nicht auf Lager', 'Остаток:': 'Bestand:', 'От': 'Von', 'шт': 'Stk',
'кор.': 'Kart.', 'уп.': 'Pck.', '+ Упаковка': '+ Verpackung', '+ Коробка': '+ Karton',
'Сумма заказа:': 'Bestellsumme:', 'Корзина': 'Warenkorb', 'Ваш заказ': 'Ihre Bestellung',
'Имя клиента (необязательно)': 'Kundenname (optional)', 'Ваше Имя': 'Ihr Name',
'Номер телефона': 'Telefonnummer', 'Город': 'Stadt', 'Адрес доставки': 'Lieferadresse',
'Индекс': 'Postleitzahl', 'Оформить заказ': 'Bestellung aufgeben', 'Оформление...': 'Wird bearbeitet...',
'Админ-панель': 'Admin-Panel', 'Отчеты': 'Berichte', 'Остатки': 'Bestände',
'История накладных и заказов': 'Rechnungs- und Bestellhistorie', 'Сборка': 'Montage',
'В каталог': 'Zum Katalog', 'Выход': 'Abmelden', 'Сохранить на сервер': 'Auf Server speichern',
'Скачать с сервера': 'Vom Server herunterladen', 'Онлайн заказы': 'Online-Bestellungen',
'Пользователи (Закрытый каталог)': 'Benutzer (Geschlossener Katalog)', 'Персонал': 'Personal',
'Настройка магазина': 'Shopeinstellungen', 'Управление категориями': 'Kategorieverwaltung',
'Добавить товар': 'Produkt hinzufügen', 'Название товара': 'Produktname', 'Наличие': 'Verfügbarkeit',
'Штрих-код': 'Barcode', 'Цена за ед.': 'Preis pro Stk.', 'В уп.': 'In Pck.',
'В кор.': 'In Kart.', 'Цена за упаковку': 'Verpackungspreis', 'Мин. заказ': 'Min. Bestellung',
'Остаток на складе': 'Lagerbestand', 'Описание товара': 'Produktbeschreibung',
'Фотографии товара (до 10 шт)': 'Produktbilder (bis zu 10)', 'Сохранить товар': 'Produkt speichern',
'Да': 'Ja', 'Нет': 'Nein', 'Варианты товара': 'Produktvarianten', 'Язык интерфейса': 'Spracheneinstellung',
'Учет коробок': 'Kartonverwaltung', 'Включить учет больших коробок': 'Verwaltung großer Kartons aktivieren',
'Общая скидка на чек (сумма)': 'Gesamtrabatt (Betrag)',
'WhatsApp клиента (напр. +77001234567) необязательно': 'Kunden WhatsApp (optional)',
'Режим кассы: Быстрое оформление': 'Kassenmodus: Schnelle Bearbeitung', 'Сохраненные кассы': 'Gespeicherte Kassen',
'Новая касса': 'Neue Kasse', 'Свой товар': 'Eigenes Produkt', 'Заказы поставщикам': 'Lieferantenbestellungen',
'Долги': 'Schulden', 'История заказов': 'Bestellhistorie', 'Текущие остатки': 'Aktuelle Bestände',
'Заканчивающиеся': 'Knapp werdend', 'История операций': 'Aktionsverlauf', 'Общее количество товаров': 'Gesamtanzahl der Produkte',
'Общая стоимость склада': 'Gesamtwert des Lagers', 'ед.': 'Stk.', 'Сделать заказ': 'Bestellung aufgeben',
'Приход': 'Eingang', 'Списание': 'Abschreibung', 'Товар': 'Produkt', 'Категория': 'Kategorie', 'Действие': 'Aktion', 'Загрузка...': 'Laden...',
'пачка': 'Packung', 'кв.м': 'qm', 'пог. м': 'lfm', 'кг': 'kg', 'л': 'l', 'Назад в панель': 'Zurück zum Panel',
'Подтвержден': 'Bestätigt', 'Касса': 'Kasse', 'Возврат': 'Rückgabe', 'Ожидает': 'Ausstehend', 'Касса / Онлайн': 'Kasse / Online',
'Накладная': 'Rechnung', 'Вы уверены, что хотите удалить накладную? Товары вернутся на склад и удалятся из отчетов.': 'Möchten Sie die Rechnung wirklich löschen? Produkte kehren ins Lager zurück.',
'Поиск по номеру, клиенту или сотруднику...': 'Suche nach Nummer, Kunde oder Mitarbeiter...', 'Период:': 'Zeitraum:', 'Сбросить': 'Zurücksetzen',
'Номер': 'Nummer', 'Дата': 'Datum', 'Клиент / Сотрудник': 'Kunde / Mitarbeiter', 'Сумма': 'Summe', 'Долг': 'Schuld', 'Статус': 'Status', 'Действия': 'Aktionen',
'История накладных': 'Rechnungshistorie', 'Сборка накладной': 'Rechnungskompletierung', 'Мои продажи (Возврат)': 'Meine Verkäufe (Rückgabe)',
'Мои накладные': 'Meine Rechnungen', 'Мои продажи': 'Meine Verkäufe', 'Возвраты': 'Rückgaben',
'Store Name': 'Geschäftsname', 'Subtitle': 'Untertitel', 'Business Type': 'Geschäftsart',
'Retail': 'Einzelhandel', 'Retail + Wholesale': 'Einzelhandel + Großhandel', 'Wholesale': 'Großhandel',
'Currency': 'Währung', 'Polski złoty (zł)': 'Polnischer Złoty (zł)', 'Euro (€)': 'Euro (€)', 'US Dollar ($)': 'US-Dollar ($)', 'Українська гривня (₴)': 'Ukrainische Hrywnja (₴)',
'Order Messenger': 'Bestell-Messenger', 'Order Contact': 'Bestellkontakt', 'Invoice Contacts': 'Rechnungskontakte', 'Logo': 'Logo', 'QR code 1': 'QR-Code 1', 'QR code 2': 'QR-Code 2',
'Customer Fields (Checkout)': 'Kundenfelder (Kasse)', 'Name': 'Name', 'Phone': 'Telefon', 'City': 'Stadt', 'Address': 'Adresse', 'Zip': 'Postleitzahl',
'Inventory': 'Inventar', 'Large Box Tracking': 'Tracking großer Kartons', 'Track stock': 'Bestand verfolgen', 'Barcodes': 'Barcodes',
'Profit Tracking (Cost Price)': 'Gewinnverfolgung (Einkaufspreis)', 'Hide stock online': 'Bestand online verbergen',
'Catalog Access': 'Katalogzugang', 'Password-protected catalog': 'Passwortgeschützter Katalog',
'Commission Fee': 'Provision', 'Enable Fee': 'Provision aktivieren', 'Percent (e.g. 0.95)': 'Prozent (z.B. 0.95)',
'Security': 'Sicherheit', 'Admin Password': 'Admin-Passwort', 'Set Password': 'Passwort festlegen',
'Social Links': 'Soziale Links', 'Save Settings': 'Einstellungen speichern', 'Wipe Data': 'Daten löschen', 'Clear Order History': 'Bestellhistorie löschen',
'UX/UI Design': 'UX/UI Design', 'Theme': 'Thema', 'Primary Color (HEX)': 'Hauptfarbe (HEX)', 'Secondary Color (HEX)': 'Sekundärfarbe (HEX)', 'Font': 'Schriftart',
'Header Style': 'Kopfzeilen-Stil', 'Card Style': 'Karten-Stil', 'Border Radius': 'Eckenradius', 'Shadow Intensity': 'Schattenintensität', 'Image Aspect Ratio': 'Bildseitenverhältnis',
'Animation Speed': 'Animationsgeschwindigkeit', 'Button Shape': 'Tastenform', 'Rounded': 'Abgerundet', 'Pill': 'Pille', 'Square': 'Quadratisch',
'List View for Categories': 'Listenansicht für Kategorien', 'Compact Mode': 'Kompaktmodus', 'Save Design': 'Design speichern',
'Category Name': 'Kategoriename', 'Category Photo (512x512)': 'Kategoriefoto (512x512)', 'Add': 'Hinzufügen', 'Search products...': 'Produkte suchen...', 'Delete Category?': 'Kategorie löschen?',
'Edit Product': 'Produkt bearbeiten', 'Status': 'Status', 'Available': 'Verfügbar', 'Out of Stock': 'Nicht auf Lager', 'Unit': 'Einheit', 'Price': 'Preis', 'Cost Price': 'Einkaufspreis',
'In Pack': 'Im Pack', 'In Box': 'Im Karton', 'Pack Price': 'Packpreis', 'Min Order': 'Mindestbestellmenge', 'Stock': 'Bestand', 'Wholesale Prices': 'Großhandelspreise', 'From': 'Von', 'Add Tier': 'Stufe hinzufügen',
'Variants': 'Varianten', 'Variant Prices': 'Variantenpreise', 'Add Variant': 'Variante hinzufügen', 'Description': 'Beschreibung', 'Photos (up to 10)': 'Fotos (bis zu 10)', 'Remove': 'Entfernen',
'Save Product': 'Produkt speichern', 'Delete product?': 'Produkt löschen?', 'Showing 50 of': 'Zeige 50 von', 'Cancel': 'Abbrechen',
'Current Month': 'Aktueller Monat', 'General': 'Allgemein', 'Daily': 'Täglich', 'By Category': 'Nach Kategorie', 'By Staff': 'Nach Mitarbeiter', 'Profit & Margin': 'Gewinn & Marge',
'Total Revenue': 'Gesamteinnahmen', 'Orders Count': 'Anzahl Bestellungen', 'Avg Order Value': 'Durchschn. Bestellwert', 'Items Sold': 'Verkaufte Artikel', 'Returns': 'Retouren', 'Commissions': 'Provisionen',
'Top Products': 'Top Produkte', 'Qty': 'Menge', 'Sum': 'Summe', 'Daily Sales': 'Tägliche Verkäufe', 'Date': 'Datum', 'Orders': 'Bestellungen', 'Revenue': 'Einnahmen',
'Net Profit': 'Nettogewinn', 'COGS (Cost)': 'Selbstkosten', 'Avg Margin': 'Durchschn. Marge', 'Profit by Product': 'Gewinn nach Produkt', 'Rev': 'Einnahmen', 'Cost': 'Kosten', 'Profit': 'Gewinn', 'Margin %': 'Marge %',
'Import / Export': 'Import / Export', 'Export': 'Exportieren', 'Download JSON DB.': 'JSON-DB herunterladen.', 'Download JSON': 'JSON herunterladen', 'Import': 'Importieren',
'Upload JSON DB.': 'JSON-DB hochladen.', 'Sample': 'Muster', 'Append / Update': 'Anhängen / Aktualisieren', 'Replace all': 'Alles ersetzen', 'Upload': 'Hochladen',
'W złote': 'In Złoty', 'Link (Online)': 'Link (Online)', 'Link (POS)': 'Link (POS)', 'Client': 'Kunde', 'Staff': 'Personal', 'Invoice': 'Rechnung', 'Discount': 'Rabatt', 'Confirm': 'Bestätigen', 'Delete': 'Löschen',
'Вход в панель': 'Ins Panel einloggen', 'Введите пароль': 'Passwort eingeben', 'Войти': 'Einloggen', 'Закрытый каталог': 'Geschlossener Katalog', 'Введите 6-значный пароль для доступа': 'Geben Sie ein 6-stelliges Passwort ein',
'Пароль': 'Passwort', 'Управление': 'Verwaltung', 'Среды': 'Umgebungen', 'Создать новую среду': 'Neue Umgebung erstellen', 'Поиск...': 'Suche...', 'Активные среды': 'Aktive Umgebungen',
'Сброс': 'Zurücksetzen', 'Архив сред': 'Umgebungsarchiv', 'Восстановить': 'Wiederherstellen', 'Окончательно': 'Dauerhaft', 'Сохранить': 'Speichern', 'Отмена': 'Abbrechen',
'Добавить долг': 'Schuld hinzufügen', 'Имя клиента': 'Kundenname', 'Сумма долга': 'Schuldbetrag', 'Нет активных долгов': 'Keine aktiven Schulden', 'Оплатить': 'Bezahlen',
'История пуста': 'Verlauf ist leer', 'Оплата долга': 'Schuldentilgung', 'Возникновение долга': 'Entstehung von Schulden', 'Нет данных': 'Keine Daten', 'Печать': 'Drucken',
'Кол': 'Anz', 'Итог': 'Gesamt', 'Скидка': 'Rabatt', 'Комиссия': 'Provision', 'К ОПЛАТЕ': 'ZU ZAHLEN', 'Оплачено': 'Bezahlt', 'В ДОЛГ': 'SCHULDEN', 'СПАСИБО ЗА ПОКУПКУ!': 'DANKE FÜR IHREN EINKAUF!',
'ТОВАРНЫЙ ЧЕК': 'KASSENBON', 'Покупатель': 'Käufer', 'Тел': 'Tel', 'Кассир': 'Kassierer', 'Pic': 'Bild', 'Expected': 'Erwartet', 'Full': 'Komplett', 'Partial': 'Teilweise', 'Type': 'Typ',
'Comment': 'Kommentar', 'Receipt from supplier': 'Eingang vom Lieferanten', 'Активные долги': 'Aktive Schulden', 'История долгов': 'Schuldenhistorie', 'Накладные': 'Rechnungen',
'Посл. Дата': 'Letztes Datum', 'Общая сумма': 'Gesamtbetrag', 'Внести оплату': 'Zahlung leisten', 'Наличные': 'Bargeld', 'Безналичные': 'Karte/Überweisung',
'New Product in': 'Neues Produkt in', 'Color, size': 'Farbe, Größe', 'Per unit': 'Pro Stück', 'Opt': 'Großhandel', 'Edit': 'Bearbeiten', 'Save': 'Speichern', 'Add Product': 'Produkt hinzufügen', 'No online orders': 'Keine Online-Bestellungen', 'Admin (Manual)': 'Admin (Manuell)', 'Manual debt added': 'Manuelle Schuld hinzugefügt', 'Group Payment': 'Gruppenzahlung', 'Debt payment': 'Schuldentilgung', 'В остатки': 'Ins Inventar', 'Заказ товара': 'Produktbestellung', 'Количество': 'Menge', 'Подтвердить': 'Bestätigen', 'Сборка накладной №': 'Rechnungskompletierung Nr.', 'Нужно:': 'Benötigt:', 'Закончить сборку': 'Kompletierung abschließen', 'Завершить сборку?': 'Kompletierung abschließen?', 'Ссылка скопирована!': 'Link kopiert!', 'Скопировать ссылку': 'Link kopieren', 'Назад к списку': 'Zurück zur Liste', 'Общее кол-во': 'Gesamtanzahl', 'Применена общая скидка': 'Gesamtrabatt angewendet', 'Итого': 'Gesamtsumme', 'Вариант:': 'Variante:', '2 в 1': '2 in 1', 'Внутренний учет': 'Interne Buchhaltung', 'Внешний учет': 'Externe Buchhaltung', 'Лайт внешка': 'Extern (Light)', 'Админ': 'Admin', 'Processing...': 'Wird bearbeitet...', 'Adding...': 'Wird hinzugefügt...', 'Error': 'Fehler', 'Link copied!': 'Link kopiert!', 'Delete order?': 'Bestellung löschen?'
},
'en': {
'Каталог': 'Catalog', 'Поиск товаров...': 'Search products...', 'Поиск': 'Search',
'Ничего не найдено': 'Nothing found', 'В этой категории пока нет товаров': 'No products in this category yet',
'В упаковке:': 'In package:', 'В коробке:': 'In box:', 'Мин. заказ:': 'Min. order:',
'Нет в наличии': 'Out of stock', 'Остаток:': 'Stock:', 'От': 'From', 'шт': 'pcs',
'кор.': 'box', 'уп.': 'pack', '+ Упаковка': '+ Package', '+ Коробка': '+ Box',
'Сумма заказа:': 'Order amount:', 'Корзина': 'Cart', 'Ваш заказ': 'Your order',
'Имя клиента (необязательно)': 'Customer name (optional)', 'Ваше Имя': 'Your Name',
'Номер телефона': 'Phone number', 'Город': 'City', 'Адрес доставки': 'Delivery address',
'Индекс': 'ZIP Code', 'Оформить заказ': 'Place order', 'Оформление...': 'Processing...',
'Админ-панель': 'Admin Panel', 'Отчеты': 'Reports', 'Остатки': 'Inventory',
'История накладных и заказов': 'Invoice and Order History', 'Сборка': 'Assembly',
'В каталог': 'To catalog', 'Выход': 'Logout', 'Сохранить на сервер': 'Save to server',
'Скачать с сервера': 'Download from server', 'Онлайн заказы': 'Online orders',
'Пользователи (Закрытый каталог)': 'Users (Closed catalog)', 'Персонал': 'Staff',
'Настройка магазина': 'Store settings', 'Управление категориями': 'Manage categories',
'Добавить товар': 'Add product', 'Название товара': 'Product name', 'Наличие': 'Availability',
'Штрих-код': 'Barcode', 'Цена за ед.': 'Price per unit', 'В уп.': 'In pack',
'В кор.': 'In box', 'Цена за упаковку': 'Price per package', 'Мин. заказ': 'Min order',
'Остаток на складе': 'Stock balance', 'Описание товара': 'Product description',
'Фотографии товара (до 10 шт)': 'Product photos (up to 10)', 'Сохранить товар': 'Save product',
'Да': 'Yes', 'Нет': 'No', 'Варианты товара': 'Product variants', 'Язык интерфейса': 'Interface language',
'Учет коробок': 'Box tracking', 'Включить учет больших коробок': 'Enable large box tracking',
'Общая скидка на чек (сумма)': 'Total discount on receipt (amount)',
'WhatsApp клиента (напр. +77001234567) необязательно': 'Customer WhatsApp (optional)',
'Режим кассы: Быстрое оформление': 'POS mode: Quick checkout', 'Сохраненные кассы': 'Saved registers',
'Новая касса': 'New register', 'Свой товар': 'Custom product', 'Заказы поставщикам': 'Supplier orders',
'Долги': 'Debts', 'История заказов': 'Order history', 'Текущие остатки': 'Current stock',
'Заканчивающиеся': 'Running low', 'История операций': 'Operation history', 'Общее количество товаров': 'Total number of products',
'Общая стоимость склада': 'Total warehouse value', 'ед.': 'units', 'Сделать заказ': 'Place order',
'Приход': 'Incoming', 'Списание': 'Write-off', 'Товар': 'Product', 'Категория': 'Category', 'Действие': 'Action', 'Загрузка...': 'Loading...',
'пачка': 'pack', 'кв.м': 'sq.m', 'пог. м': 'lin.m', 'кг': 'kg', 'л': 'l', 'Назад в панель': 'Back to panel',
'Подтвержден': 'Confirmed', 'Касса': 'POS', 'Возврат': 'Return', 'Ожидает': 'Pending', 'Касса / Онлайн': 'POS / Online',
'Накладная': 'Invoice', 'Вы уверены, что хотите удалить накладную? Товары вернутся на склад и удалятся из отчетов.': 'Are you sure you want to delete the invoice? Items will return to stock.',
'Поиск по номеру, клиенту или сотруднику...': 'Search by number, customer, or staff...', 'Период:': 'Period:', 'Сбросить': 'Reset',
'Номер': 'Number', 'Дата': 'Date', 'Клиент / Сотрудник': 'Customer / Staff', 'Сумма': 'Amount', 'Долг': 'Debt', 'Статус': 'Status', 'Действия': 'Actions',
'История накладных': 'Invoice history', 'Сборка накладной': 'Invoice assembly', 'Мои продажи (Возврат)': 'My sales (Return)',
'Мои накладные': 'My invoices', 'Мои продажи': 'My sales', 'Возвраты': 'Returns',
'Вход в панель': 'Panel Login', 'Введите пароль': 'Enter password', 'Войти': 'Login', 'Закрытый каталог': 'Closed Catalog', 'Введите 6-значный пароль для доступа': 'Enter 6-digit password', 'Пароль': 'Password', 'Управление': 'Management', 'Среды': 'Environments', 'Создать новую среду': 'Create new environment', 'Поиск...': 'Search...', 'Активные среды': 'Active environments', 'Сброс': 'Reset', 'Архив сред': 'Environment Archive', 'Восстановить': 'Restore', 'Окончательно': 'Permanently', 'Сохранить': 'Save', 'Отмена': 'Cancel', 'Добавить долг': 'Add debt', 'Имя клиента': 'Customer name', 'Сумма долга': 'Debt amount', 'Нет активных долгов': 'No active debts', 'Оплатить': 'Pay', 'История пуста': 'History is empty', 'Оплата долга': 'Debt payment', 'Возникновение долга': 'Debt occurrence', 'Нет данных': 'No data', 'Печать': 'Print', 'Кол': 'Qty', 'Итог': 'Total', 'Скидка': 'Discount', 'Комиссия': 'Commission', 'К ОПЛАТЕ': 'TO PAY', 'Оплачено': 'Paid', 'В ДОЛГ': 'DEBT', 'СПАСИБО ЗА ПОКУПКУ!': 'THANK YOU FOR YOUR PURCHASE!', 'ТОВАРНЫЙ ЧЕК': 'RECEIPT', 'Покупатель': 'Buyer', 'Тел': 'Tel', 'Кассир': 'Cashier', 'Pic': 'Pic', 'Expected': 'Expected', 'Full': 'Full', 'Partial': 'Partial', 'Type': 'Type', 'Comment': 'Comment', 'Receipt from supplier': 'Receipt from supplier', 'Активные долги': 'Active debts', 'История долгов': 'Debt history', 'Накладные': 'Invoices', 'Посл. Дата': 'Last Date', 'Общая сумма': 'Total amount', 'Внести оплату': 'Make payment', 'Наличные': 'Cash', 'Безналичные': 'Card/Transfer', 'В остатки': 'To inventory', 'Заказ товара': 'Order product', 'Количество': 'Quantity', 'Подтвердить': 'Confirm', 'Сборка накладной №': 'Assembly of Invoice #', 'Нужно:': 'Needed:', 'Закончить сборку': 'Finish assembly', 'Завершить сборку?': 'Finish assembly?', 'Ссылка скопирована!': 'Link copied!', 'Скопировать ссылку': 'Copy link', 'Назад к списку': 'Back to list', 'Общее кол-во': 'Total quantity', 'Применена общая скидка': 'Global discount applied', 'Итого': 'Total', 'Вариант:': 'Variant:', '2 в 1': '2 in 1', 'Внутренний учет': 'Internal accounting', 'Внешний учет': 'External accounting', 'Лайт внешка': 'Light external', 'Админ': 'Admin', 'Добавить': 'Add', 'Списание': 'Write-off'
},
'uk': {
'Каталог': 'Каталог', 'Поиск товаров...': 'Пошук товарів...', 'Поиск': 'Пошук',
'Ничего не найдено': 'Нічого не знайдено', 'В этой категории пока нет товаров': 'У цій категорії поки немає товарів',
'В упаковке:': 'В упаковці:', 'В коробке:': 'В коробці:', 'Мин. заказ:': 'Мін. замовлення:',
'Нет в наличии': 'Немає в наявності', 'Остаток:': 'Залишок:', 'От': 'Від', 'шт': 'шт',
'кор.': 'кор.', 'уп.': 'уп.', '+ Упаковка': '+ Упаковка', '+ Коробка': '+ Коробка',
'Сумма заказа:': 'Сума замовлення:', 'Корзина': 'Кошик', 'Ваш заказ': 'Ваше замовлення',
'Имя клиента (необязательно)': 'Ім\'я клієнта (необов\'язково)', 'Ваше Имя': 'Ваше Ім\'я',
'Номер телефона': 'Номер телефону', 'Город': 'Місто', 'Адрес доставки': 'Адреса доставки',
'Индекс': 'Індекс', 'Оформить заказ': 'Оформити замовлення', 'Оформление...': 'Оформлення...',
'Админ-панель': 'Адмін-панель', 'Отчеты': 'Звіти', 'Остатки': 'Залишки',
'История накладных и заказов': 'Історія накладних і замовлень', 'Сборка': 'Збірка',
'В каталог': 'В каталог', 'Выход': 'Вихід', 'Сохранить на сервер': 'Зберегти на сервер',
'Скачать с сервера': 'Завантажити з сервера', 'Онлайн заказы': 'Онлайн замовлення',
'Пользователи (Закрытый каталог)': 'Користувачі (Закритий каталог)', 'Персонал': 'Персонал',
'Настройка магазина': 'Налаштування магазину', 'Управление категориями': 'Управління категоріями',
'Добавить товар': 'Додати товар', 'Название товара': 'Назва товару', 'Наличие': 'Наявність',
'Штрих-код': 'Штрих-код', 'Цена за ед.': 'Ціна за од.', 'В уп.': 'В уп.',
'В кор.': 'В кор.', 'Цена за упаковку': 'Ціна за упаковку', 'Мин. заказ': 'Мін. замовлення',
'Остаток на складе': 'Залишок на складі', 'Описание товара': 'Опис товару',
'Фотографии товара (до 10 шт)': 'Фотографії товару (до 10 шт)', 'Сохранить товар': 'Зберегти товар',
'Да': 'Так', 'Нет': 'Ні', 'Варианты товара': 'Варіанти товару', 'Язык интерфейса': 'Мова інтерфейсу',
'Учет коробок': 'Облік коробок', 'Включить учет больших коробок': 'Увімкнути облік великих коробок',
'Общая скидка на чек (сумма)': 'Загальна знижка на чек (сума)',
'WhatsApp клиента (напр. +77001234567) необязательно': 'WhatsApp клієнта (необов\'язково)',
'Режим кассы: Быстрое оформление': 'Режим каси: Швидке оформлення', 'Сохраненные кассы': 'Збережені каси',
'Новая касса': 'Нова каса', 'Свой товар': 'Свій товар', 'Заказы поставщикам': 'Замовлення постачальникам',
'Долги': 'Борги', 'История заказов': 'Історія замовлень', 'Текущие остатки': 'Поточні залишки',
'Заканчивающиеся': 'Закінчуються', 'История операций': 'Історія операцій', 'Общее количество товаров': 'Загальна кількість товарів',
'Общая стоимость склада': 'Загальна вартість складу', 'ед.': 'од.', 'Сделать заказ': 'Зробити замовлення',
'Приход': 'Прихід', 'Списание': 'Списання', 'Товар': 'Товар', 'Категория': 'Категорія', 'Действие': 'Дія', 'Загрузка...': 'Завантаження...',
'пачка': 'пачка', 'кв.м': 'кв.м', 'пог. м': 'пог. м', 'кг': 'кг', 'л': 'л', 'Назад в панель': 'Назад у панель',
'Подтвержден': 'Підтверджено', 'Касса': 'Каса', 'Возврат': 'Повернення', 'Ожидает': 'Очікує', 'Касса / Онлайн': 'Каса / Онлайн',
'Накладная': 'Накладна', 'Вы уверены, что хотите удалить накладную? Товары вернутся на склад и удалятся из отчетов.': 'Ви впевнені, що хочете видалити накладну? Товари повернуться на склад.',
'Поиск по номеру, клиенту или сотруднику...': 'Пошук за номером, клієнтом або співробітником...', 'Период:': 'Період:', 'Сбросить': 'Скинути',
'Номер': 'Номер', 'Дата': 'Дата', 'Клиент / Сотрудник': 'Клієнт / Співробітник', 'Сумма': 'Сума', 'Долг': 'Борг', 'Статус': 'Статус', 'Действия': 'Дії',
'История накладных': 'Історія накладних', 'Сборка накладной': 'Збірка накладної', 'Мои продажи (Возврат)': 'Мої продажі (Повернення)',
'Мои накладные': 'Мої накладні', 'Мои продажи': 'Мої продажі', 'Возвраты': 'Повернення',
'Store Name': 'Назва магазину', 'Subtitle': 'Підзаголовок', 'Business Type': 'Тип бізнесу', 'Retail': 'Роздріб', 'Retail + Wholesale': 'Роздріб + Опт', 'Wholesale': 'Опт',
'Currency': 'Валюта', 'Polski złoty (zł)': 'Польський злотий (zł)', 'Euro (€)': 'Євро (€)', 'US Dollar ($)': 'Долар США ($)', 'Українська гривня (₴)': 'Українська гривня (₴)',
'Order Messenger': 'Месенджер замовлень', 'Order Contact': 'Контакт замовлень', 'Invoice Contacts': 'Контакти накладних', 'Logo': 'Логотип', 'QR code 1': 'QR-код 1', 'QR code 2': 'QR-код 2',
'Customer Fields (Checkout)': 'Поля клієнта (Оформлення)', 'Name': 'Ім\'я', 'Phone': 'Телефон', 'City': 'Місто', 'Address': 'Адреса', 'Zip': 'Індекс', 'Inventory': 'Склад', 'Large Box Tracking': 'Відстеження великих коробок',
'Track stock': 'Відстежувати залишки', 'Barcodes': 'Штрих-коди', 'Profit Tracking (Cost Price)': 'Відстеження прибутку (Собівартість)', 'Hide stock online': 'Приховати залишки онлайн',
'Catalog Access': 'Доступ до каталогу', 'Password-protected catalog': 'Каталог під паролем', 'Commission Fee': 'Комісія', 'Enable Fee': 'Увімкнути комісію', 'Percent (e.g. 0.95)': 'Відсоток (напр. 0.95)',
'Security': 'Безпека', 'Admin Password': 'Пароль адміна', 'Set Password': 'Встановити пароль', 'Social Links': 'Соціальні посилання', 'Save Settings': 'Зберегти налаштування', 'Wipe Data': 'Стерти дані',
'Clear Order History': 'Очистити історію замовлень', 'UX/UI Design': 'Дизайн UX/UI', 'Theme': 'Тема', 'Primary Color (HEX)': 'Основний колір (HEX)', 'Secondary Color (HEX)': 'Додатковий колір (HEX)', 'Font': 'Шрифт',
'Header Style': 'Стиль заголовка', 'Card Style': 'Стиль картки', 'Border Radius': 'Радіус рамок', 'Shadow Intensity': 'Інтенсивність тіні', 'Image Aspect Ratio': 'Пропорції зображення',
'Animation Speed': 'Швидкість анімації', 'Button Shape': 'Форма кнопки', 'Rounded': 'Закруглені', 'Pill': 'Пігулка', 'Square': 'Квадрат', 'List View for Categories': 'Вигляд списком для категорій', 'Compact Mode': 'Компактний режим',
'Save Design': 'Зберегти дизайн', 'Category Name': 'Назва категорії', 'Category Photo (512x512)': 'Фото категорії (512x512)', 'Delete Category?': 'Видалити категорію?', 'Edit Product': 'Редагувати товар', 'Available': 'В наявності',
'Out of Stock': 'Немає в наявності', 'Unit': 'Одиниця', 'Price': 'Ціна', 'Cost Price': 'Собівартість', 'In Pack': 'В упаковці', 'In Box': 'В коробці', 'Pack Price': 'Ціна за упаковку', 'Min Order': 'Мін. замовлення', 'Stock': 'Залишок',
'Wholesale Prices': 'Оптові ціни', 'From': 'Від', 'Add Tier': 'Додати рівень', 'Variants': 'Варіанти', 'Variant Prices': 'Ціни варіантів', 'Add Variant': 'Додати варіант', 'Description': 'Опис', 'Photos (up to 10)': 'Фотографії (до 10)',
'Remove': 'Видалити', 'Save Product': 'Зберегти товар', 'Delete product?': 'Видалити товар?', 'Showing 50 of': 'Показано 50 з', 'Cancel': 'Скасувати', 'Current Month': 'Поточний місяць', 'General': 'Загальне', 'Daily': 'Щоденно',
'By Category': 'За категорією', 'By Staff': 'За персоналом', 'Profit & Margin': 'Прибуток і маржа', 'Total Revenue': 'Загальний прибуток', 'Orders Count': 'Кількість замовлень', 'Avg Order Value': 'Середня вартість замовлення',
'Items Sold': 'Продано товарів', 'Returns': 'Повернення', 'Commissions': 'Комісії', 'Top Products': 'Топ товари', 'Qty': 'Кіл-ть', 'Sum': 'Сума', 'Daily Sales': 'Щоденні продажі', 'Date': 'Дата', 'Orders': 'Замовлення', 'Revenue': 'Дохід',
'Net Profit': 'Чистий прибуток', 'COGS (Cost)': 'Собівартість', 'Avg Margin': 'Середня маржа', 'Profit by Product': 'Прибуток за товаром', 'Rev': 'Дохід', 'Cost': 'Витрати', 'Profit': 'Прибуток', 'Margin %': 'Маржа %',
'Import / Export': 'Імпорт / Експорт', 'Export': 'Експорт', 'Download JSON DB.': 'Завантажити базу JSON.', 'Download JSON': 'Завантажити JSON', 'Import': 'Імпорт', 'Upload JSON DB.': 'Завантажити базу JSON.', 'Sample': 'Зразок',
'Append / Update': 'Додати / Оновити', 'Replace all': 'Замінити все', 'Upload': 'Завантажити', 'W złote': 'В злоті', 'Link (Online)': 'Посилання (Онлайн)', 'Link (POS)': 'Посилання (POS)', 'Client': 'Клієнт', 'Staff': 'Персонал',
'Invoice': 'Накладна', 'Discount': 'Знижка', 'Confirm': 'Підтвердити', 'Delete': 'Видалити', 'Вход в панель': 'Вхід у панель', 'Введите пароль': 'Введіть пароль', 'Войти': 'Увійти', 'Закрытый каталог': 'Закритий каталог',
'Введите 6-значный пароль для доступа': 'Введіть 6-значний пароль для доступу', 'Пароль': 'Пароль', 'Управление': 'Управління', 'Среды': 'Середовища', 'Создать новую среду': 'Створити нове середовище', 'Поиск...': 'Пошук...',
'Активные среды': 'Активні середовища', 'Сброс': 'Скинути', 'Архив сред': 'Архів середовищ', 'Восстановить': 'Відновити', 'Окончательно': 'Остаточно', 'Сохранить': 'Зберегти', 'Отмена': 'Скасувати', 'Добавить долг': 'Додати борг',
'Имя клиента': 'Ім\'я клієнта', 'Сумма долга': 'Сума боргу', 'Нет активных долгов': 'Немає активних боргів', 'Оплатить': 'Сплатити', 'История пуста': 'Історія порожня', 'Оплата долга': 'Оплата боргу', 'Возникновение долга': 'Виникнення боргу',
'Нет данных': 'Немає даних', 'Печать': 'Друк', 'Кол': 'Кіл', 'Итог': 'Підсумок', 'Скидка': 'Знижка', 'Комиссия': 'Комісія', 'К ОПЛАТЕ': 'ДО ОПЛАТИ', 'Оплачено': 'Оплачено', 'В ДОЛГ': 'БОРГ', 'СПАСИБО ЗА ПОКУПКУ!': 'ДЯКУЄМО ЗА ПОКУПКУ!',
'ТОВАРНЫЙ ЧЕК': 'ТОВАРНИЙ ЧЕК', 'Покупатель': 'Покупець', 'Тел': 'Тел', 'Кассир': 'Касир', 'Pic': 'Фото', 'Expected': 'Очікується', 'Full': 'Повністю', 'Partial': 'Частково', 'Type': 'Тип', 'Comment': 'Коментар',
'Receipt from supplier': 'Надходження від постачальника', 'Активные долги': 'Активні борги', 'История долгов': 'Історія боргів', 'Накладные': 'Накладні', 'Посл. Дата': 'Ост. Дата', 'Общая сумма': 'Загальна сума', 'Внести оплату': 'Внести оплату',
'Наличные': 'Готівка', 'Безналичные': 'Картка/Переказ', 'New Product in': 'Новий товар у', 'Color, size': 'Колір, розмір', 'Per unit': 'За одиницю', 'Opt': 'Опт', 'Edit': 'Редагувати', 'Save': 'Зберегти', 'Add Product': 'Додати товар',
'No online orders': 'Немає онлайн замовлень', 'Admin (Manual)': 'Адмін (Вручну)', 'Manual debt added': 'Борг додано вручну', 'Group Payment': 'Групова оплата', 'Debt payment': 'Оплата боргу', 'В остатки': 'В залишки', 'Заказ товара': 'Замовлення товару',
'Количество': 'Кількість', 'Подтвердить': 'Підтвердити', 'Сборка накладной №': 'Збірка накладної №', 'Нужно:': 'Потрібно:', 'Закончить сборку': 'Закінчити збірку', 'Завершить сборку?': 'Завершити збірку?', 'Ссылка скопирована!': 'Посилання скопійовано!',
'Скопировать ссылку': 'Скопіювати посилання', 'Назад к списку': 'Назад до списку', 'Общее кол-во': 'Загальна кіл-ть', 'Применена общая скидка': 'Застосована загальна знижка', 'Итого': 'Разом', 'Вариант:': 'Варіант:', '2 в 1': '2 в 1',
'Внутренний учет': 'Внутрішній облік', 'Внешний учет': 'Зовнішній облік', 'Лайт внешка': 'Лайт зовн.', 'Админ': 'Адмін', 'Processing...': 'Обробка...', 'Adding...': 'Додавання...', 'Error': 'Помилка', 'Link copied!': 'Посилання скопійовано!', 'Delete order?': 'Видалити замовлення?'
}
}
def get_t(lang='pl'):
def t(text):
if not isinstance(text, str): return text
return TRANSLATIONS.get(lang, {}).get(text, text)
return t
def get_warsaw_time():
return datetime.now(ZoneInfo('Europe/Warsaw')).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_warsaw_time()}")
except Exception: pass
except Exception: pass
def process_and_upload_image(file_obj, repo_path, size=(1024, 1024)):
if not HF_TOKEN_WRITE: return None
try:
img = Image.open(file_obj)
if img.mode in ("RGBA", "P"): img = img.convert("RGB")
img.thumbnail(size, Image.Resampling.LANCZOS)
buf = io.BytesIO()
img.save(buf, format='WEBP', quality=80, method=6)
buf.seek(0)
filename = f"{uuid4().hex}.webp"
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': 'zł',
'track_inventory': False, 'use_barcodes': False, 'use_master_boxes': False, 'margin_tracking': False, 'language': 'pl', 'business_type': 'mixed', 'system_mode': 'both',
'hide_stock_online': False, 'closed_catalog_enabled': False, 'commission_enabled': False, 'commission_percent': 0.0, 'categories_list_view': False, 'button_shape': 'rounded',
'theme': 'light', 'primary_color': '#007aff', 'font_family': 'Montserrat', 'card_style': 'shadow', 'is_deleted': False,
'secondary_color': '#34c759', 'border_radius_level': 'medium', 'shadow_intensity': 'medium', 'header_style': 'glass', 'image_aspect_ratio': '1:1', 'compact_mode': False, 'animation_speed': 'normal',
'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': ''} }
}
}
}
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 'button_shape' not in settings: settings['button_shape'] = 'rounded'; changed = True
if 'primary_color' not in settings: settings['primary_color'] = '#007aff'; changed = True
if 'font_family' not in settings: settings['font_family'] = 'Montserrat'; changed = True
if 'card_style' not in settings: settings['card_style'] = 'shadow'; changed = True
if 'currency' not in settings: settings['currency'] = 'zł'; changed = True
elif settings['currency'] in ['T', '₸']: settings['currency'] = 'zł'; 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'] = 'pl'; 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 'categories_list_view' not in settings: settings['categories_list_view'] = False; changed = True
if 'theme' not in settings: settings['theme'] = 'light'; changed = True
if 'secondary_color' not in settings: settings['secondary_color'] = '#34c759'; changed = True
if 'border_radius_level' not in settings: settings['border_radius_level'] = 'medium'; changed = True
if 'shadow_intensity' not in settings: settings['shadow_intensity'] = 'medium'; changed = True
if 'header_style' not in settings: settings['header_style'] = 'glass'; changed = True
if 'image_aspect_ratio' not in settings: settings['image_aspect_ratio'] = '1:1'; changed = True
if 'compact_mode' not in settings: settings['compact_mode'] = False; changed = True
if 'animation_speed' not in settings: settings['animation_speed'] = 'normal'; 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': ''}, 'ig': {'enabled': True, 'url': ''}, 'tg': {'enabled': True, 'url': ''}, '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 'unit' not in product: product['unit'] = 'шт'; 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 'unit' not in item: item['unit'] = 'шт'; 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': "+48123456789", 'order_messenger': 'wa', 'order_contact': "+48123456789", 'invoice_contacts': '', 'currency': 'zł',
'track_inventory': False, 'use_barcodes': False, 'use_master_boxes': False, 'margin_tracking': False, 'language': 'pl', 'business_type': 'mixed', 'system_mode': 'both',
'hide_stock_online': False, 'closed_catalog_enabled': False, 'commission_enabled': False, 'commission_percent': 0.0, 'categories_list_view': False, 'button_shape': 'rounded',
'theme': 'light', 'primary_color': '#007aff', 'secondary_color': '#34c759', 'border_radius_level': 'medium', 'shadow_intensity': 'medium', 'header_style': 'glass', 'image_aspect_ratio': '1:1', 'compact_mode': False, 'animation_speed': 'normal',
'font_family': 'Montserrat', 'card_style': 'shadow', '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('cart_price', 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
if not i.get('custom_price_set'):
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 = '''
{{ t('Вход в панель') }}
{{ t('Вход в панель') }}
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
'''
CATALOG_LOGIN_TEMPLATE = '''
{{ t('Закрытый каталог') }}
{{ t('Закрытый каталог') }}
{{ t('Введите 6-значный пароль для доступа') }}
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
'''
ADMHOSTO_TEMPLATE = '''
{{ t('Управление') }}
{{ t('Среды') }}
{% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %}
{{ t('Активные среды') }}
{{ t('Архив сред') }}
{% 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' %}
{{ t('Касса') }} (1)
{% if staff_id %}
{{ t('Мои продажи') }}
{{ t('Возвраты') }}
{% 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.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 }}
{% if currency_code != 'zł' and mode == 'pos' %}
{{ t('W złote') }}
{% endif %}
{{ t('Корзина') }}
{{ t('Оформить заказ') }}
'''
ORDER_TEMPLATE = '''
{{ t('Накладная') }} №{{ 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 %}
{{ t('Имя клиента (необязательно)') }}: {{ order.customer_name }}
{% endif %}
{% if order.customer_phone %}
{{ t('Номер телефона') }}: {{ order.customer_phone }}
{% endif %}
{% if order.customer_city %}
{{ t('Город') }}: {{ order.customer_city }}
{% endif %}
{% if order.customer_address %}
{{ t('Адрес доставки') }}: {{ order.customer_address }}
{% endif %}
{% if order.customer_zip %}
{{ t('Индекс') }}: {{ order.customer_zip }}
{% endif %}
{% else %}
{{ t('Имя клиента (необязательно)') }}: {{ order.customer_name if order.customer_name else t('Касса / Онлайн') }}
{% if order.customer_whatsapp %}
WhatsApp: {{ order.customer_whatsapp }}
{% endif %}
{% endif %}
{% if order.staff_name %}
{{ t('Персонал') }}: {{ order.staff_name }}
{% endif %}
{{ t('Статус') }}:
{% if order.status == 'pending' %}{{ t('Ожидает') }}
{% elif order.status == 'confirmed' %}{{ t('Подтвержден') }}
{% elif order.status == 'pos' %}{{ t('Касса') }}
{% elif order.status == 'returned' %}{{ t('Возврат') }}
{% else %}{{ order.status }} {% endif %}
{% if order.is_debt %}
{{ t('Долг') }}: {{ order.debt_amount }} {{ currency_code }}
{% endif %}
{% if total_customer_debt and total_customer_debt > 0 %}
{{ t('Долг') }} {{ total_customer_debt|round(2) }} {{ currency_code }}
{% endif %}
№ {{ t('Товар') }} {{ t('Pic') }} {{ t('Qty') }} {{ t('Unit') }} {{ t('Sum') }}
{% 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 unit = item.unit|default(t('шт')) %}
{% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %}
{% if item.quantity > 0 %}
{{ loop.index }}
{{ item.name }}
{{ t('Категория') }}: {{ item.category }}
{% if item.variant_name %}{{ t('Варианты товара') }}: {{ item.variant_name }}
{% endif %}
{{ t('Сборка') }}: {{ assembled }} / {{ item.quantity }}
{% if item.discount and item.discount > 0 %}{{ t('Discount') }}: -{{ item.discount }} {{ currency_code }} {{ t('Per unit') }}
{% endif %}
{% if order.status == 'pending' or is_admin %}
{% 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 }} {{ unit }}{% 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 }} {{ unit }}{% endif %}
{% else %}
{{ item.quantity }} {{ unit }}
{% endif %}
{% else %}
{% 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 }} {{ unit }}{% 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 }} {{ unit }}{% endif %}
{% else %}
{{ item.quantity }} {{ unit }}
{% endif %}
{{ item.calculated_price | round(2) }}
{% if is_admin %}
{{ t('Цена') }}
{% endif %}
{{ (item.calculated_price * item.quantity) | round(2) }}
{% endif %}
{% endfor %}
{{ t('Общее кол-во') }}: {{ total_qty }} {{ t('ед.') }}
{% if order.global_discount > 0 %}{{ t('Применена общая скидка') }}: -{{ order.global_discount }} {{ currency_code }}
{% endif %}
{% if settings.commission_enabled and order.commission_amount and order.commission_amount > 0 %}{{ t('Комиссия') }} ({{ settings.commission_percent }}%): +{{ order.commission_amount }} {{ currency_code }}
{% endif %}
{{ t('Итого') }}: {{ 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 %}
{{ t('ТОВАРНЫЙ ЧЕК') }} № {{ order.id }}
{{ order.created_at }}
{% if order.status != 'pos' and order.status != 'returned' %}
{% if order.customer_name %}
{{ t('Покупатель') }}: {{ order.customer_name }}
{% endif %}
{% if order.customer_phone %}
{{ t('Тел') }}: {{ order.customer_phone }}
{% endif %}
{% else %}
{{ t('Покупатель') }}: {{ order.customer_name if order.customer_name else 'POS' }}
{% if order.customer_whatsapp %}
WA: {{ order.customer_whatsapp }}
{% endif %}
{% endif %}
{% if order.staff_name %}
{{ t('Кассир') }}: {{ order.staff_name }}
{% endif %}
{{ t('Товар') }} {{ t('Кол') }} {{ t('Цена') }} {{ t('Сумма') }}
{% for item in order.cart %}
{% if item.quantity > 0 %}
{{ item.name }}{% if item.variant_name %} ({{ item.variant_name }}){% endif %}
{{ item.quantity }}
{{ item.calculated_price | round(2) }}
{{ (item.calculated_price * item.quantity) | round(2) }}
{% endif %}
{% endfor %}
{{ t('Итог') }} ({{ total_qty }} {{ t('ед.') }}): {{ order.total_price }} {{ currency_code }}
{% if order.global_discount > 0 %}
{{ t('Скидка') }}: -{{ order.global_discount }} {{ currency_code }}
{% endif %}
{% if settings.commission_enabled and order.commission_amount and order.commission_amount > 0 %}
{{ t('Комиссия') }}: +{{ order.commission_amount }} {{ currency_code }}
{% endif %}
{{ t('К ОПЛАТЕ') }}: {{ order.total_price }} {{ currency_code }}
{% if order.is_debt %}
{{ t('Оплачено') }}: {{ order.paid_amount }} {{ currency_code }}
{{ t('В ДОЛГ') }}: {{ order.debt_amount }} {{ currency_code }}
{% endif %}
{% if settings.qr_code_1_url %}
{% endif %}
{% if settings.qr_code_2_url %}
{% endif %}
*** {{ t('СПАСИБО ЗА ПОКУПКУ!') }} ***
{{ t('Печать') }}
{% set messenger = settings.order_messenger|default('wa') %}
{% if order.status == 'pos' %}
WhatsApp
{% else %}
{% if messenger == 'tg' %}
Telegram
{% else %}
WhatsApp
{% endif %}
{% endif %}
'''
ASSEMBLY_TEMPLATE = '''
{{ t('Сборка накладной №') }} {{ 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 %}{% set unit = item.unit|default(t('шт')) %}
{{ item.name }}
{% if item.variant_name %}
{{ t('Вариант:') }} {{ item.variant_name }}
{% endif %}
{{ t('Нужно:') }} {{ item.quantity }} {{ unit }}
{% if assembled >= item.quantity %}{{ t('Собрано') }}{% else %}В процессе{% endif %}
{% endif %}{% endfor %}
{{ t('Закончить сборку') }}
'''
HISTORY_TEMPLATE = '''
{{ t(page_title) }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
{{ t('Номер') }}
{{ t('Дата') }}
{{ t('Клиент / Сотрудник') }}
{{ t('Сумма') }} ({{ currency_code }})
{{ t('Долг') }}
{{ t('Статус') }}
{{ t('Действия') }}
'''
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 %}
{{ t('Client') }}: {{ order.customer_name }} ({{ order.customer_phone }})
{% if order.staff_name %}
{{ t('Staff') }}: {{ order.staff_name }}
{% endif %}
Sum: {{ order.total_price }} {{ order.currency|default(currency_code) }}
{{ t('Discount') }} for #{{ order.id }}
{% endfor %}
{% else %}
{{ t('No online orders') }}
{% endif %}
{% if sys_mode != 'internal' %}
{% for u in catalog_users %}
{{ u.name }}
Password: {{ u.password }}
{% endfor %}
{% endif %}
{% if sys_mode != 'light_external' %}
{% for s in staff %}
{{ s.name }} ({{ s.whatsapp }})
{% if sys_mode != 'internal' %}
{{ t('Link (Online)') }}
{% endif %}
{% if sys_mode != 'external' %}
{{ t('Link (POS)') }}
{% endif %}
{% endfor %}
{% endif %}
{{ t('Import / Export') }}
{{ t('Wipe Data') }}:
{{ t('Clear Order History') }}
{{ t('Theme') }}:
{{ t('Light') }}
{{ t('Dark') }}
{{ t('Magma') }}
{{ t('Ocean') }}
{{ t('Forest') }}
{{ t('Cyberpunk') }}
{{ t('Font') }}:
Montserrat
Inter
Roboto
Playfair Display
{{ t('Header Style') }}:
{{ t('Glassmorphism') }}
{{ t('Solid') }}
{{ t('Gradient') }}
{{ t('Card Style') }}:
{{ t('Shadow') }}
{{ t('Flat') }}
{{ t('Bordered') }}
{{ t('Border Radius') }}:
{{ t('Small') }}
{{ t('Medium') }}
{{ t('Large') }}
{{ t('Max') }}
{{ t('Shadow Intensity') }}:
{{ t('Light') }}
{{ t('Medium') }}
{{ t('Strong') }}
{{ t('Image Aspect Ratio') }}:
{{ t('Square (1:1)') }}
{{ t('Classic (4:3)') }}
{{ t('Portrait (3:4)') }}
{{ t('Wide (16:9)') }}
{{ t('Animation Speed') }}:
{{ t('Fast') }}
{{ t('Normal') }}
{{ t('Smooth') }}
{{ t('List View for Categories') }}
{{ t('Compact Mode') }}
{{ t('Save Design') }}
{{ t('Управление категориями') }}
{{ t('Add') }}
{% 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 %}
{{ t('Save') }}
{{ t('Add Product') }}
{{ t('New Product in') }} "{{ category }}"
{% if settings.business_type == 'wholesale' %}
{{ t('Wholesale Prices') }}
{{ t('Add Tier') }}
{% endif %}
{{ t('Description') }}
{{ t('Photos (up to 10)') }}
{{ t('Save Product') }}
{% 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[:60] }}{{ '...' if product.description|length > 60 else '' }}
{% endif %}
{% if product.is_available == False %}
{{ t('Out of Stock') }} •
{% endif %}
{% if product.has_variant_prices %}
{{ t('Variant Prices') }}
{% else %}
{{ product.price }} {{ currency_code }} / {{ product.unit|default(t('шт')) }}
{% if settings.business_type == 'mixed' and product.box_price %} ({{ t('Pack Price') }}: {{ product.box_price }}){% endif %}
{% if settings.margin_tracking and sys_mode == 'both' and product.cost_price %}
| {{ t('Cost Price') }}: {{ product.cost_price }} {{ currency_code }}
{% endif %}
{% endif %}
{% if settings.business_type != 'retail' %}
• {{ t('In Pack') }}: {{ product.pieces_per_box|default(1) }} {{ product.unit|default(t('шт')) }}
{% if settings.use_master_boxes and product.pieces_per_master_box %}
• {{ t('In Box') }}: {{ product.pieces_per_master_box }} {{ product.unit|default(t('шт')) }}
{% endif %}
{% endif %}
{% if settings.business_type == 'wholesale' and product.min_order %}
• {{ t('Min Order') }}: {{ product.min_order }} {{ product.unit|default(t('шт')) }}
{% endif %}
{% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% if not product.variants %}
• {{ t('Stock') }}: {{ product.stock if product.stock != "" else "0" }}
{% else %}
• {{ t('Stock') }} per variant
{% endif %}
{% endif %}
{{ t('Edit Product') }}
{% if settings.business_type == 'wholesale' %}
{{ t('Wholesale Prices') }}
{{ t('Add Tier') }}
{% endif %}
{{ t('Variants') }}
{{ t('Variant Prices') }}
{% for variant in product.variants %}
{% endfor %}
{{ t('Add Variant') }}
{{ t('Description') }}
{{ product.description }}
{{ t('Save') }}
{% endif %}
{% set ns.count = ns.count + 1 %}
{% endif %}
{% endif %}
{% endfor %}
{% if ns.count > 50 and not search_q %}
{{ t('Showing 50 of') }} {{ ns.count }}.
{% endif %}
{% endfor %}
{{ render_pagination(cat_page, total_pages, 'page', '&q=' ~ search_q) }}
'''
REPORTS_TEMPLATE = '''
{{ t('Отчеты') }}
{{ t('General') }}
{{ t('Daily') }}
{{ t('By Category') }}
{{ t('By Staff') }}
{% if settings.margin_tracking and settings.system_mode == 'both' %}
{{ t('Profit & Margin') }}
{% endif %}
{{ t('Total Revenue') }}
0
{{ t('Orders Count') }}
0
{{ t('Avg Order Value') }}
0
{% if settings.commission_enabled %}
{{ t('Commissions') }} ({{ settings.commission_percent }}%)
0
{% endif %}
{{ t('Top Products') }}
{{ t('Товар') }}
{{ t('Qty') }}
{{ t('Sum') }} ({{ currency_code }})
{{ t('Daily Sales') }}
{{ t('Date') }}
{{ t('Orders') }}
{{ t('Revenue') }} ({{ currency_code }})
{{ t('By Category') }}
{{ t('Категория') }}
{{ t('Qty') }}
{{ t('Revenue') }} ({{ currency_code }})
{{ t('By Staff') }}
{{ t('Staff') }}
{{ t('Orders') }}
{{ t('Revenue') }} ({{ currency_code }})
{% if settings.margin_tracking and settings.system_mode == 'both' %}
{{ t('Profit by Product') }}
{{ t('Товар') }}
{{ t('Qty') }}
{{ t('Rev') }}
{{ t('Cost') }}
{{ t('Profit') }}
{{ t('Margin %') }}
{% endif %}
'''
INVENTORY_TEMPLATE = '''
{{ t('Остатки') }}
{{ t('Общее количество товаров') }}
{{ total_qty }} {{ t('ед.') }}
{{ t('Общая стоимость склада') }}
{{ total_value }} {{ settings.currency }}
{{ t('Текущие остатки') }}
{{ t('Заканчивающиеся') }} {{ low_stock_items|length }}
{{ t('Заказы поставщикам') }} {{ active_purchases|length }}
{{ t('История операций') }}
{{ t('Товар') }}
{{ t('Варианты товара') }}
{{ t('Категория') }}
{{ t('Остаток') }}
{% for item in low_stock_items %}
{{ item.name }}
{{ item.variant }}
{{ item.category }}
{{ item.stock }}
{% else %}
{{ t("Нет данных") }}
{% endfor %}
{{ t('Дата') }}
{{ t('Товар') }}
{{ t('Expected') }} ({{ t('ед.') }})
{{ t('Действия') }}
{% for po in active_purchases %}
{{ po.date }}
{{ po.product_name }} {% if po.variant_name %}{{ po.variant_name }} {% endif %}
{{ po.qty }}
{% else %}
{{ t("Нет данных") }}
{% endfor %}
{{ t('Дата') }}
{{ t('Товар') }}
{{ t('Type') }}
{{ t('Qty') }}
{{ t('Comment') }}
{% for h in history|reverse %}
{{ h.date }}
{{ h.product }} {% if h.variant %}({{ h.variant }}) {% endif %}
{% if h.change > 0 %}
{{ t('Приход') }}
{% else %}
{{ t('Списание') }}
{% endif %}
{{ h.change|abs }}
{{ h.comment }}
{% else %}
{{ t("Нет данных") }}
{% endfor %}
'''
DEBTS_TEMPLATE = '''
{{ t('Долги') }}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
{{ t('Активные долги') }}
{{ t('История долгов') }}
{{ t('Дата') }}
{{ t('Клиент') }}
{{ t('Накладная') }} / Info
{{ t('Действие') }}
{{ t('Сумма') }}
{% for h in debt_history %}
{{ h.date }}
{{ h.customer }}
{% if h.order_id != 'Group Payment' and h.order_id != 'Групповая оплата' %}
{{ h.order_id }}
{% else %}
{{ t(h.order_id) }}
{% endif %}
{{ t(h.action) }}
{{ h.amount }} {{ currency }}
{% else %}
{{ t('История пуста') }}
{% endfor %}
'''
PURCHASES_TEMPLATE = '''
{{ t('Заказы поставщикам') }}
{{ t('Товар') }}
{{ t('Категория') }}
{{ t('Остаток') }}
{{ t('Действие') }}
{% for p in products %}
{% if not p.variants %}
{{ p.name }}
ID: {{ p.product_id }}
{{ p.category }}
{{ p.stock if p.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
{{ t('Сделать заказ') }}
{% else %}
{% for v in p.variants %}
{{ p.name }} ({{ v.name }})
ID: {{ p.product_id }}
{{ p.category }}
{{ v.stock if v.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
{{ t('Сделать заказ') }}
{% endfor %}
{% endif %}
{% endfor %}
'''
@app.route('/')
def index():
return render_template_string(LANDING_PAGE_TEMPLATE)
@app.route('//set_lang/')
def set_lang(env_id, lang):
session[f'lang_{env_id}'] = lang
return redirect(request.referrer or url_for('catalog', env_id=env_id))
@app.route('/admhosto', methods=['GET'])
def admhosto():
data = load_data()
active_envs = []
archived_envs = []
t = get_t('ru') # default fallback
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, t=t)
@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": "zł",
"track_inventory": False, "use_barcodes": False, "use_master_boxes": False, "margin_tracking": False, "language": "pl", "business_type": "mixed", "system_mode": "both",
"hide_stock_online": False, "closed_catalog_enabled": False, "commission_enabled": False, "commission_percent": 0.0, "categories_list_view": False, "button_shape": "rounded",
"theme": "light", "primary_color": "#007aff", "secondary_color": "#34c759", "border_radius_level": "medium", "shadow_intensity": "medium", "header_style": "glass", "image_aspect_ratio": "1:1", "compact_mode": False, "animation_speed": "normal", "font_family": "Montserrat", "card_style": "shadow", "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', {})
t = get_t(settings.get('language', 'pl'))
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(t('Error'), 'error')
return render_template_string(LOGIN_TEMPLATE, env_id=env_id, t=t)
@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', {})
user_lang = session.get(f'lang_{env_id}', settings.get('language', 'pl'))
t = get_t(user_lang)
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(t('Error'), 'error')
return render_template_string(CATALOG_LOGIN_TEMPLATE, env_id=env_id, t=t)
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', 'zł'), settings=settings, env_id=env_id, mode=mode, staff_id=staff_id, t=t, user_lang=user_lang)
@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=(1024, 1024))
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
order_currency = order_data.get('currency', data['settings'].get('currency', 'zł'))
exchange_rate = order_data.get('exchange_rate', 1)
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, "unit": item.get('unit', 'szt'), "variant_name": '',
"variant_idx": -1, "discount": float(item.get('discount', 0)), "wholesale_tiers": [], "category": 'Własny produkt',
"photo_url": photo_url, "is_custom": True, "custom_price_set": item.get('custom_price_set', False)
})
continue
p_info = product_dict.get(item.get('product_id'), {})
cat = p_info.get('category', 'Без категории')
photo_url = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjNmBmMHcwIi8+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,
"unit": item.get('unit', 'szt'), "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, "custom_price_set": item.get('custom_price_set', 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_warsaw_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,
"currency": order_currency, "exchange_rate": exchange_rate, "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_warsaw_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', 'pl'))
if not order: return "Order not found", 404
total_customer_debt = 0
c_name = (order.get('customer_name') or '').strip().lower()
if c_name:
for o in data.get('orders', {}).values():
if o.get('is_debt') and (o.get('customer_name') or '').strip().lower() == c_name:
total_customer_debt += float(o.get('debt_amount', 0))
order_currency = order.get('currency', settings.get('currency', 'zł'))
is_admin = session.get(f'admin_auth_{env_id}', False)
return render_template_string(ORDER_TEMPLATE, order=order, settings=settings, currency_code=order_currency, env_id=env_id, t=t, total_customer_debt=total_customer_debt, is_admin=is_admin)
@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
t = get_t(settings.get('language', 'pl'))
return render_template_string(ASSEMBLY_TEMPLATE, order=order, env_id=env_id, settings=settings, t=t)
@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
is_admin = session.get(f'admin_auth_{env_id}', False)
if order.get('status') != 'pending' and not is_admin:
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')
new_price = req_data.get('new_price')
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)
elif change: item['quantity'] += change
if new_price is not None:
item['cart_price'] = float(new_price)
item['custom_price_set'] = True
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('Discount can only be applied to pending orders.', '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('Discount applied successfully.', '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('//delete_order/', methods=['POST'])
def delete_order(env_id, order_id):
data = get_env_data(env_id)
order = data.get('orders', {}).get(order_id)
t = get_t(data.get('settings', {}).get('language', 'pl'))
if not order:
flash(t('Error'), 'error')
return redirect(url_for('history', env_id=env_id))
track_inv = data['settings'].get('track_inventory', False) and data['settings'].get('system_mode', 'both') not in ['external', 'light_external']
if order.get('status') in ['confirmed', 'pos'] and track_inv:
for item in order.get('cart', []):
if not item.get('is_custom'):
restore_stock(item.get('c_key'), item.get('product_id'), item.get('variant_idx', -1), item.get('quantity', 0), data['products'])
if order.get('is_debt'):
data['debt_history'] = [h for h in data.get('debt_history', []) if h.get('order_id') != order_id]
del data['orders'][order_id]
save_env_data(env_id, data)
flash(t('Confirm'), 'success')
return redirect(url_for('history', env_id=env_id))
@app.route('//add_manual_debt', methods=['POST'])
def add_manual_debt(env_id):
data = get_env_data(env_id)
t = get_t(data.get('settings', {}).get('language', 'pl'))
customer_name = request.form.get('customer_name', '').strip()
amount = float(request.form.get('amount', 0))
if customer_name and amount > 0:
order_id = f"MANUAL-{uuid4().hex[:8].upper()}"
new_order = {
"id": order_id, "created_at": get_warsaw_time(), "cart": [], "status": "pos",
"staff_id": "", "staff_name": "Admin (Manual)", "customer_name": customer_name,
"total_price": amount, "is_debt": True, "paid_amount": 0, "debt_amount": amount,
"currency": data['settings'].get('currency', 'zł')
}
data['orders'][order_id] = new_order
if 'debt_history' not in data: data['debt_history'] = []
data['debt_history'].append({
'id': uuid4().hex, 'date': get_warsaw_time(), 'order_id': order_id,
'customer': customer_name, 'action': 'Manual debt added', 'amount': amount
})
save_env_data(env_id, data)
flash(t('Confirm'), 'success')
return redirect(url_for('debts', env_id=env_id))
@app.route('//pay_debt_grouped', methods=['POST'])
def pay_debt_grouped(env_id):
data = get_env_data(env_id)
t = get_t(data.get('settings', {}).get('language', 'pl'))
cname_lower = request.form.get('customer_name_lower', '').strip()
amount = float(request.form.get('amount', 0))
payment_method = request.form.get('payment_method', 'Cash')
if amount > 0 and cname_lower:
all_orders = list(data.get('orders', {}).values())
customer_debts = [o for o in all_orders if o.get('is_debt', False) and float(o.get('debt_amount', 0)) > 0 and (o.get('customer_name') or '').strip().lower() == cname_lower]
customer_debts.sort(key=lambda x: x.get('created_at', ''))
remaining_payment = amount
original_cname = customer_debts[0].get('customer_name') or 'POS' if customer_debts else 'POS'
for o in customer_debts:
if remaining_payment <= 0: break
debt = float(o.get('debt_amount', 0))
if debt <= remaining_payment:
o['paid_amount'] = round(float(o.get('total_price', 0)), 2)
o['debt_amount'] = 0
o['is_debt'] = False
remaining_payment -= debt
else:
o['paid_amount'] = round(float(o.get('paid_amount', 0)) + remaining_payment, 2)
o['debt_amount'] = round(float(o.get('total_price', 0)) - o['paid_amount'], 2)
remaining_payment = 0
if 'debt_history' not in data: data['debt_history'] = []
data['debt_history'].append({
'id': uuid4().hex, 'date': get_warsaw_time(), 'order_id': 'Group Payment',
'customer': original_cname, 'action': f'Debt payment ({payment_method})', 'amount': amount
})
save_env_data(env_id, data)
flash(t('Confirm'), 'success')
return redirect(url_for('debts', env_id=env_id))
@app.route('//pay_debt/', methods=['POST'])
def pay_debt(env_id, order_id):
data = get_env_data(env_id)
t = get_t(data.get('settings', {}).get('language', 'pl'))
order = data.get('orders', {}).get(order_id)
if order and order.get('is_debt'):
amount = float(request.form.get('amount', 0))
payment_method = request.form.get('payment_method', 'Cash')
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_warsaw_time(), 'order_id': order_id,
'customer': order.get('customer_name') or 'POS', 'action': f'Debt payment ({payment_method})', 'amount': amount
})
save_env_data(env_id, data)
flash(t('Confirm'), '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))
t = get_t(settings.get('language', 'pl'))
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)
grouped_debts_dict = {}
for o in debt_orders:
cname = (o.get('customer_name') or '').strip()
cname_lower = cname.lower()
if cname_lower not in grouped_debts_dict:
grouped_debts_dict[cname_lower] = {
'customer_name': cname,
'customer_name_lower': cname_lower,
'customer_whatsapp': o.get('customer_whatsapp', ''),
'latest_date': o.get('created_at', '').split(' ')[0],
'total_price': 0,
'paid_amount': 0,
'debt_amount': 0,
'order_ids': []
}
g = grouped_debts_dict[cname_lower]
g['total_price'] = round(g['total_price'] + float(o.get('total_price', 0)), 2)
g['paid_amount'] = round(g['paid_amount'] + float(o.get('paid_amount', 0)), 2)
g['debt_amount'] = round(g['debt_amount'] + float(o.get('debt_amount', 0)), 2)
g['order_ids'].append(o['id'])
grouped_debts = list(grouped_debts_dict.values())
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, grouped_debts=grouped_debts, debt_history=history_list, settings=settings, currency=settings.get('currency', 'zł'), t=t)
@app.route('//history')
def history(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
t = get_t(settings.get('language', 'pl'))
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', 'zł'), orders_json=json.dumps(orders_list), page_title=page_title, settings=settings, t=t)
@app.route('//assembly_list')
def assembly_list(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
t = get_t(settings.get('language', 'pl'))
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', 'zł'), orders_json=json.dumps(unassembled), page_title='Сборка', settings=settings, t=t)
@app.route('//reports')
def reports(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
t = get_t(settings.get('language', 'pl'))
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', 'zł'), orders_json=json.dumps(orders_list), t=t)
@app.route('//inventory')
def inventory(env_id):
data = get_env_data(env_id)
settings = data.get('settings', {})
t = get_t(settings.get('language', 'pl'))
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))
products = data.get('products', [])
total_qty = 0
total_value = 0
for p in products:
price = float(p.get('price', 0) or 0)
if p.get('variants'):
for v in p['variants']:
s = v.get('stock')
if s != "" and s is not None:
try:
v_qty = int(s)
if v_qty > 0:
total_qty += v_qty
v_price = float(v.get('price', 0) or price) if p.get('has_variant_prices') else price
total_value += v_qty * v_price
except ValueError: pass
else:
s = p.get('stock')
if s != "" and s is not None:
try:
m_qty = int(s)
if m_qty > 0:
total_qty += m_qty
total_value += m_qty * price
except ValueError: pass
low_stock_items = get_low_stock_items(products)
active_purchases = data.get('purchase_orders', [])
return render_template_string(INVENTORY_TEMPLATE, env_id=env_id, settings=settings, products=products, history=data.get('inventory_history',[]), low_stock_items=low_stock_items, active_purchases=active_purchases, total_qty=total_qty, total_value=round(total_value, 2), t=t)
@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_warsaw_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', {})
t = get_t(settings.get('language', 'pl'))
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, settings=settings, products=data.get('products', []), t=t)
@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_warsaw_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_warsaw_time(), "product": p['name'], "variant": po['variant_name'],
"change": qty_received, "comment": "Receipt from supplier"
})
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', 'pl'))
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(t('Confirm'), '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 must contain list of products")
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_warsaw_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(t('Confirm'), 'success')
except Exception as e: flash(t('Error'), 'error')
else: flash(t('Error'), 'error')
elif action == 'update_settings':
settings['language'] = request.form.get('language', 'pl')
settings['organization_name'] = request.form.get('organization_name', '').strip()
settings['catalog_subtitle'] = request.form.get('catalog_subtitle', '').strip()
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', 'zł')
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)
flash(t('Confirm'), 'success')
elif action == 'update_design_settings':
settings['theme'] = request.form.get('theme', 'light')
settings['button_shape'] = request.form.get('button_shape', 'rounded')
settings['categories_list_view'] = 'categories_list_view' in request.form
settings['primary_color'] = request.form.get('primary_color', '#007aff')
settings['secondary_color'] = request.form.get('secondary_color', '#34c759')
settings['border_radius_level'] = request.form.get('border_radius_level', 'medium')
settings['shadow_intensity'] = request.form.get('shadow_intensity', 'medium')
settings['header_style'] = request.form.get('header_style', 'glass')
settings['image_aspect_ratio'] = request.form.get('image_aspect_ratio', '1:1')
settings['animation_speed'] = request.form.get('animation_speed', 'normal')
settings['compact_mode'] = 'compact_mode' in request.form
settings['font_family'] = request.form.get('font_family', 'Montserrat')
settings['card_style'] = request.form.get('card_style', 'shadow')
data['settings'] = settings
save_env_data(env_id, data)
flash(t('Confirm'), 'success')
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=(1024, 1024))
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=(1024, 1024))
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=(1024, 1024))
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:
for photo in uploaded_photos:
if photo and photo.filename:
filename = process_and_upload_image(photo, 'photos', size=(1024, 1024))
if filename: photos_list.append(filename)
new_product = {
'product_id': uuid4().hex, 'created_at': get_warsaw_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, 'unit': request.form.get('unit_selector', 'шт')
}
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
unit = request.form.get('unit_selector', 'шт')
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=(1024, 1024))
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:
for photo in uploaded_photos:
if photo and photo.filename:
filename = process_and_upload_image(photo, 'photos', size=(1024, 1024))
if filename: photos_list.append(filename)
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
p['unit'] = unit
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', 'zł'), 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)