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('Архив сред') }}

''' CATALOG_TEMPLATE = ''' {{ settings.organization_name }} {% if mode == 'pos' %}
{{ t('Режим кассы: Быстрое оформление') }}
{% endif %}
PL | DE | EN | UK
{% if settings.catalog_subtitle %}
{{ settings.catalog_subtitle }}
{% endif %}

{{ settings.organization_name }}

{% if mode == 'pos' %}
{% if staff_id %} {% endif %}
{% endif %}
{% if mode != 'pos' %} {% if settings.socials.wa.enabled and settings.socials.wa.url %} {% for link in settings.socials.wa.url.split() %}{% if link.strip() %}{% endif %}{% endfor %} {% endif %} {% if settings.socials.ig.enabled and settings.socials.ig.url %} {% for link in settings.socials.ig.url.split() %}{% if link.strip() %}{% endif %}{% endfor %} {% endif %} {% if settings.socials.tg.enabled and settings.socials.tg.url %} {% for link in settings.socials.tg.url.split() %}{% if link.strip() %}{% endif %}{% endfor %} {% endif %} {% if settings.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' %} {% endif %}
''' 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 %}

{{ 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 %} {% 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 %}
{% 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 %} {% endif %} {% endfor %}
{{ t('Товар') }}{{ t('Pic') }}{{ t('Qty') }}{{ t('Unit') }}{{ t('Sum') }}
{{ 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 %}
img {% 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 %}
{{ item.calculated_price | round(2) }} {% if is_admin %}
{% endif %}
{{ (item.calculated_price * item.quantity) | round(2) }}
{{ 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 %}QR Code 1{% endif %} {% if settings.qr_code_2_url %}QR Code 2{% endif %}
{{ settings.organization_name }}
{% if settings.invoice_contacts %}
{% for contact in settings.invoice_contacts.split(',') %}{% if contact.strip() %}
{{ contact.strip() }}
{% endif %}{% endfor %}
{% endif %}
{{ 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 %}
{% for item in order.cart %} {% if item.quantity > 0 %} {% endif %} {% endfor %}
{{ t('Товар') }}{{ t('Кол') }}{{ t('Цена') }}{{ t('Сумма') }}
{{ item.name }}{% if item.variant_name %} ({{ item.variant_name }}){% endif %} {{ item.quantity }} {{ item.calculated_price | round(2) }} {{ (item.calculated_price * item.quantity) | round(2) }}
{{ 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('СПАСИБО ЗА ПОКУПКУ!') }} ***
{% set messenger = settings.order_messenger|default('wa') %} {% if order.status == 'pos' %} {% else %} {% if messenger == 'tg' %} {% else %} {% endif %} {% endif %}
''' ASSEMBLY_TEMPLATE = ''' {{ t('Сборка накладной №') }} {{ order.id }}
{{ t('Назад к списку') }}

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

{{ order.created_at }}
{% for item in order.cart %}{% if item.quantity > 0 %}{% set assembled = order.assembled.get(item.c_key, 0) if order.assembled else 0 %}{% 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 %}
''' HISTORY_TEMPLATE = ''' {{ t(page_title) }}

{{ t(page_title) }}

{{ t('Назад в панель') }}
{{ t('Период:') }}
{% 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') %}

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

{% if sys_mode != 'light_external' %} {{ t('Отчеты') }} {% endif %} {% if sys_mode not in ['external', 'light_external'] %} {{ t('Остатки') }} {% if low_stock_count > 0 %}{{ low_stock_count }}{% endif %} {% if sys_mode == 'both' %} {{ t('Заказы поставщикам') }} {% endif %} {{ t('Долги') }} {% endif %} {% if sys_mode == 'both' %} {{ t('История накладных и заказов') }} {% elif sys_mode in ['external', 'light_external'] %} {{ t('История заказов') }} {% else %} {{ t('История накладных') }} {% endif %} {% if sys_mode != 'light_external' %} {{ t('Сборка') }} {% if unassembled_count > 0 %}{{ unassembled_count }}{% endif %} {% endif %} {% if sys_mode != 'internal' %} {{ t('В каталог') }} {% endif %} {% if settings.admin_password_enabled %} {{ t('Выход') }} {% endif %}
{{ t('Онлайн заказы') }} {% if pending_orders|length > 0 %}{{ pending_orders|length }}{% endif %}
{% if pending_orders %} {% for order in pending_orders %}
Order #{{ order.id }} {{ order.created_at }}
{{ 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('Invoice') }}
{% endfor %} {% else %}

{{ t('No online orders') }}

{% endif %}
{% if sys_mode != 'internal' %}
{{ t('Пользователи (Закрытый каталог)') }}
{% for u in catalog_users %}
{{ u.name }} Password: {{ u.password }}
{% endfor %}
{% endif %} {% if sys_mode != 'light_external' %}
{{ t('Персонал') }}
{% for s in staff %}
{{ s.name }} ({{ s.whatsapp }})
{% if sys_mode != 'internal' %} {% endif %} {% if sys_mode != 'external' %} {% endif %}
{% endfor %}
{% endif %}

{{ t('Import / Export') }}

{{ t('Export') }}

{{ t('Download JSON DB.') }}

{{ t('Download JSON') }}

{{ t('Import') }}

{{ t('Upload JSON DB.') }} {{ t('Sample') }}

{{ t('Настройка магазина') }}
{% if settings.qr_code_1_url %} {% endif %}
{% if settings.qr_code_2_url %} {% endif %}
{{ t('Wipe Data') }}:
{{ t('UX/UI Design') }}

{{ t('Управление категориями') }}

{% set search_q = request.args.get('q', '').lower() %} {% set cat_page = request.args.get('page', 1)|int %} {% set per_page = 10 %}
{% set filtered_cats = [] %} {% if search_q %} {% for c in categories %} {% set ns_cat = namespace(match=false) %} {% if search_q in c|lower %} {% set ns_cat.match = true %} {% else %} {% for p in products %} {% if p.category == c %} {% if search_q in p.name|lower or search_q in p.product_id|lower or (p.barcode and search_q in p.barcode|lower) %} {% set ns_cat.match = true %} {% else %} {% for v in p.variants %} {% if v.barcode and search_q in v.barcode|lower %} {% set ns_cat.match = true %} {% endif %} {% endfor %} {% endif %} {% endif %} {% endfor %} {% endif %} {% if ns_cat.match %} {% set _ = filtered_cats.append(c) %} {% endif %} {% endfor %} {% else %} {% set filtered_cats = categories %} {% endif %} {% set total_cats = filtered_cats|length %} {% set total_pages = (total_cats / per_page)|round(0, 'ceil')|int %} {% if total_pages == 0 %}{% set total_pages = 1 %}{% endif %} {% set start_idx = (cat_page - 1) * per_page %} {% set end_idx = start_idx + per_page %} {% set paged_cats = filtered_cats[start_idx:end_idx] %} {% for category in paged_cats %}
{{ category }}
{% if category_photos.get(category) %}
{% endif %}
{{ t('Add Product') }}
{{ t('New Product in') }} "{{ category }}"
{% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.margin_tracking and sys_mode == 'both' %}
{% endif %} {% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% endif %} {% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type == 'wholesale' %}
{% endif %}
{% set ns = namespace(count=0) %} {% for product in products %} {% if product.category == category %} {% set ns_prod = namespace(match=false) %} {% if not search_q %} {% set ns_prod.match = true %} {% else %} {% if search_q in product.name|lower or search_q in product.product_id|lower or search_q in category|lower or (product.barcode and search_q in product.barcode|lower) %} {% set ns_prod.match = true %} {% else %} {% for v in product.variants %} {% if v.barcode and search_q in v.barcode|lower %} {% set ns_prod.match = true %} {% endif %} {% endfor %} {% endif %} {% endif %} {% if ns_prod.match %} {% if ns.count < 50 or search_q %}
{% if product.photos and product.photos|length > 0 %} {% else %}
{% endif %}
{{ product.name }}
ID: {{ product.product_id }}
{% if product.description %} {{ product.description[: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.use_barcodes and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.margin_tracking and sys_mode == 'both' %}
{% endif %} {% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% endif %} {% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %}
{% if settings.business_type == 'wholesale' %}
{% for tier in product.wholesale_tiers %}
{% endfor %}
{% endif %}
{% for variant in product.variants %}
{% if variant.photo %}
{% endif %}
{% if settings.business_type != 'retail' %}
{% if settings.use_master_boxes %}
{% endif %} {% endif %} {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
{% endif %}
{% if settings.margin_tracking and sys_mode == 'both' %}
{% endif %} {% if settings.business_type == 'mixed' %}
{% endif %} {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
{% endif %} {% if settings.business_type == 'wholesale' %}
{% for tier in variant.wholesale_tiers %}
{% endfor %}
{% endif %}
{% endfor %}
{% if product.photos and product.photos|length > 0 %}
{% for ph in product.photos %}
{% endfor %}
{% endif %}
{% endif %} {% set ns.count = ns.count + 1 %} {% endif %} {% endif %} {% endfor %} {% if ns.count > 50 and not search_q %}
{{ t('Showing 50 of') }} {{ ns.count }}.
{% endif %}
{% endfor %} {{ render_pagination(cat_page, total_pages, 'page', '&q=' ~ search_q) }}
''' REPORTS_TEMPLATE = ''' {{ t('Отчеты') }}

{{ t('Отчеты') }}

{{ t('Назад в панель') }}
{{ 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
{{ t('Items Sold') }}
0
{{ t('Returns') }}
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('Net Profit') }}
0
{{ t('COGS (Cost)') }}
0
{{ t('Avg Margin') }}
0%

{{ t('Profit by Product') }}

{{ t('Товар') }} {{ t('Qty') }} {{ t('Rev') }} {{ t('Cost') }} {{ t('Profit') }} {{ t('Margin %') }}
{% endif %}
''' INVENTORY_TEMPLATE = ''' {{ t('Остатки') }}

{{ t('Остатки') }}

{{ t('Назад в панель') }}
{{ t('Общее количество товаров') }}
{{ total_qty }} {{ t('ед.') }}
{{ t('Общая стоимость склада') }}
{{ total_value }} {{ settings.currency }}
{{ t('Текущие остатки') }}
{{ t('Заканчивающиеся') }} {{ low_stock_items|length }}
{{ t('Заказы поставщикам') }} {{ active_purchases|length }}
{{ t('История операций') }}
{% for p in products %} {% if not p.variants %} {% else %} {% for v in p.variants %} {% endfor %} {% endif %} {% endfor %}
{{ t('Товар') }} {{ t('Категория') }} {{ t('Остаток') }} {{ t('Действие') }} ({{ t('Приход') }} / {{ t('Списание') }})
{{ p.name }}
ID: {{ p.product_id }}
{{ p.category }} {{ p.stock if p.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
{{ p.name }} ({{ v.name }})
ID: {{ p.product_id }}
{{ p.category }} {{ v.stock if v.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
{% for item in low_stock_items %} {% else %} {% endfor %}
{{ t('Товар') }} {{ t('Варианты товара') }} {{ t('Категория') }} {{ t('Остаток') }}
{{ item.name }} {{ item.variant }} {{ item.category }} {{ item.stock }}
{{ t("Нет данных") }}
{% for po in active_purchases %} {% else %} {% endfor %}
{{ t('Дата') }} {{ t('Товар') }} {{ t('Expected') }} ({{ t('ед.') }}) {{ t('Действия') }}
{{ po.date }} {{ po.product_name }} {% if po.variant_name %}
{{ po.variant_name }}{% endif %}
{{ po.qty }}
{{ t("Нет данных") }}
{% for h in history|reverse %} {% else %} {% endfor %}
{{ t('Дата') }} {{ t('Товар') }} {{ t('Type') }} {{ t('Qty') }} {{ t('Comment') }}
{{ h.date }} {{ h.product }} {% if h.variant %}({{ h.variant }}){% endif %} {% if h.change > 0 %} {{ t('Приход') }} {% else %} {{ t('Списание') }} {% endif %} {{ h.change|abs }} {{ h.comment }}
{{ t("Нет данных") }}
''' DEBTS_TEMPLATE = ''' {{ t('Долги') }}

{{ t('Долги') }}

{{ t('Назад в панель') }}
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}
{{ t('Активные долги') }}
{{ t('История долгов') }}
{% for d in grouped_debts %} {% else %} {% endfor %}
{{ t('Накладные') }} {{ t('Посл. Дата') }} {{ t('Клиент') }} {{ t('Общая сумма') }} {{ t('Оплачено') }} {{ t('Долг') }} {{ t('Внести оплату') }}
{% for oid in d.order_ids %} {{ oid }}
{% endfor %}
{{ d.latest_date }} {{ d.customer_name if d.customer_name else 'POS' }}
{{ d.customer_whatsapp }}
{{ d.total_price }} {{ currency }} {{ d.paid_amount }} {{ currency }} {{ d.debt_amount }} {{ currency }}
{{ t('Нет активных долгов') }}
{% for h in debt_history %} {% else %} {% endfor %}
{{ t('Дата') }} {{ t('Клиент') }} {{ t('Накладная') }} / Info {{ t('Действие') }} {{ t('Сумма') }}
{{ 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 }}
{{ t('История пуста') }}
''' PURCHASES_TEMPLATE = ''' {{ t('Заказы поставщикам') }}

{{ t('Заказы поставщикам') }}

{{ t('В остатки') }} {{ t('Назад в панель') }}
{% for p in products %} {% if not p.variants %} {% else %} {% for v in p.variants %} {% endfor %} {% endif %} {% endfor %}
{{ t('Товар') }} {{ t('Категория') }} {{ t('Остаток') }} {{ t('Действие') }}
{{ p.name }}
ID: {{ p.product_id }}
{{ p.category }} {{ p.stock if p.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
{{ p.name }} ({{ v.name }})
ID: {{ p.product_id }}
{{ p.category }} {{ v.stock if v.stock != "" else "0" }} {{ p.unit|default(t('шт')) }}
''' @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)