diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,5 @@ -from flask import Flask, render_template_string, request, redirect, url_for + +from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash, send_from_directory import json import os import logging @@ -6,1160 +7,2149 @@ import threading import time from datetime import datetime from huggingface_hub import HfApi, hf_hub_download -from huggingface_hub.utils import RepositoryNotFoundError +from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename +import uuid # Для генерации уникальных ID +from decimal import Decimal, InvalidOperation # Для точной работы с метрами/сантиметрами +# --- Настройки приложения --- app = Flask(__name__) -DATA_FILE = 'data_zzirix.json' +app.secret_key = os.urandom(24) # Необходим для flash сообщений +DATA_FILE = 'data.json' # Основной файл данных +UPLOAD_FOLDER = 'uploads' # Папка для временных файлов (если нужна) +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +# --- Настройки Hugging Face --- +# !!! ВАЖНО: Установите переменные окружения HF_TOKEN_WRITE и HF_TOKEN_READ !!! +# В реальном приложении используйте python-dotenv или системные переменные +HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE", "YOUR_WRITE_TOKEN_HERE") # Замените или установите переменную окружения +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", "YOUR_READ_TOKEN_HERE") # Замените или установите переменную окружения +REPO_ID = os.getenv("HF_REPO_ID", "YourUsername/YourTextileRepo") # !!! ЗАМЕНИТЕ НА ВАШ РЕПОЗИТОРИЙ !!! +# Убедитесь, что репозиторий существует на Hugging Face как Dataset -# Настройки Hugging Face -REPO_ID = "Kgshop/clients" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +# --- Настройка логирования --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -# Ссылка на логотип -LOGO_URL = "https://huggingface.co/spaces/Kgshop/Zzirixadm/resolve/main/Picsart_25-03-20_15-38-36-600.jpg" +# --- Блокировка для безопасной работы с файлом --- +data_lock = threading.Lock() -# Настройка логирования -logging.basicConfig(level=logging.DEBUG) +# --- Вспомогательные функции для работы с данными --- def load_data(): - try: - download_db_from_hf() - with open(DATA_FILE, 'r', encoding='utf-8') as file: - data = json.load(file) - logging.info("Данные успешно загружены из JSON") - if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: - return {'products': [], 'categories': [] if not isinstance(data, list) else data} - return data - except FileNotFoundError: - logging.warning("Локальный файл базы данных не найден после скачивания.") - return {'products': [], 'categories': []} - except json.JSONDecodeError: - logging.error("Ошибка: Невозможно декодировать JSON файл.") - return {'products': [], 'categories': []} - except RepositoryNotFoundError: - logging.error("Репозиторий не найден. Создание локальной базы данных.") - return {'products': [], 'categories': []} - except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных: {e}") - return {'products': [], 'categories': []} + """Загружает данные из JSON файла, скачивая с Hugging Face при необходимости.""" + with data_lock: + try: + # Пытаемся скачать последнюю версию с HF + logging.info(f"Попытка скачивания {DATA_FILE} из репозитория {REPO_ID}...") + hf_hub_download( + repo_id=REPO_ID, + filename=DATA_FILE, + repo_type="dataset", + token=HF_TOKEN_READ, + local_dir=".", + local_dir_use_symlinks=False, + force_download=True # Всегда скачиваем последнюю версию + ) + logging.info("База данных успешно скачана из Hugging Face.") + except RepositoryNotFoundError: + logging.warning(f"Репозиторий {REPO_ID} не найден на Hugging Face. Проверяем локальный файл.") + except HfHubHTTPError as e: + if e.response.status_code == 404: + logging.warning(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID}. Проверяем локальный файл.") + else: + logging.error(f"Ошибка HTTP при скачивании из Hugging Face: {e}") + except Exception as e: + logging.error(f"Неизвестная ошибка при скачивании из Hugging Face: {e}") + + # Читаем локальный файл (скачанный или существующий) + try: + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + logging.info("Данные успешно загружены из локального JSON.") + # Проверка базовой структуры + if not isinstance(data, dict): + logging.warning("Файл данных не является словарем, инициализация пустой структурой.") + return initialize_data_structure() + # Убедимся, что все ключи существуют + default_data = initialize_data_structure() + for key in default_data.keys(): + if key not in data: + data[key] = default_data[key] + return data + except FileNotFoundError: + logging.warning(f"Локальный файл {DATA_FILE} не найден. Инициализация пустой структурой.") + return initialize_data_structure() + except json.JSONDecodeError: + logging.error(f"Ошибка декодирования JSON в файле {DATA_FILE}. Инициализация пустой структурой.") + # Можно добавить логику бэкапа поврежденного файла здесь + return initialize_data_structure() + except Exception as e: + logging.error(f"Неизвестная ошибка при загрузке локальных данных: {e}") + return initialize_data_structure() def save_data(data): - try: - with open(DATA_FILE, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) - logging.info("Данные успешно сохранены в JSON") - upload_db_to_hf() - except Exception as e: - logging.error(f"Ошибка при сохранении данных: {e}") - raise + """Сохраняет данные в JSON файл и загружает на Hugging Face.""" + with data_lock: + try: + # Сначала сохраняем локально + temp_file = DATA_FILE + ".tmp" + with open(temp_file, 'w', encoding='utf-8') as file: + # Используем собственный сериализатор для Decimal + json.dump(data, file, ensure_ascii=False, indent=4, cls=DecimalEncoder) + os.replace(temp_file, DATA_FILE) # Атомарная замена файла + logging.info(f"Данные успешно сохранены в локальный файл {DATA_FILE}.") + + # Затем загружаем на Hugging Face + upload_db_to_hf() + + except Exception as e: + logging.error(f"Критическая ошибка при сохранении данных: {e}") + # Можно добавить логику отката или повторной попытки + # Временный файл может остаться, если os.replace не сработал + if os.path.exists(temp_file): + try: + os.remove(temp_file) + except OSError as rm_err: + logging.error(f"Не удалось удалить временный файл {temp_file}: {rm_err}") + # Не вызываем upload_db_to_hf(), так как локальное сохранение могло не удасться + +def initialize_data_structure(): + """Возвращает пустую структуру данных по умолчанию.""" + return { + 'materials': [], # Закупленные материалы (ткани, фурнитура) + 'categories': [], # Список категорий материалов + 'cutting_tasks': [], # Задания на раскрой (что вырезано, сколько) + 'sewing_tasks': [], # Задания на пошив (что сшито, из чего) + 'qc_packing_items': [], # Готовые и упакованные изделия + 'defect_log': [] # Журнал брака + } def upload_db_to_hf(): + """Загружает локальный файл данных на Hugging Face.""" + if not HF_TOKEN_WRITE or HF_TOKEN_WRITE == "YOUR_WRITE_TOKEN_HERE": + logging.warning("Токен HF_TOKEN_WRITE не установлен. Загрузка на Hugging Face пропущена.") + return try: api = HfApi() api.upload_file( path_or_fileobj=DATA_FILE, - path_in_repo=DATA_FILE, + path_in_repo=DATA_FILE, # Имя файла в репозитории repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Автоматическое резервное копирование базы данных {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) - logging.info("Резервная копия JSON базы успешно загружена на Hugging Face.") - except Exception as e: - logging.error(f"Ошибка при загрузке резервной копии: {e}") - -def download_db_from_hf(): - try: - hf_hub_download( - repo_id=REPO_ID, - filename=DATA_FILE, - repo_type="dataset", - token=HF_TOKEN_READ, - local_dir=".", - local_dir_use_symlinks=False - ) - logging.info("JSON база успешно скачана из Hugging Face.") - except RepositoryNotFoundError as e: - logging.error(f"Репозиторий не найден: {e}") - raise + logging.info(f"Резервная копия {DATA_FILE} успешно загружена на Hugging Face в репозиторий {REPO_ID}.") + except RepositoryNotFoundError: + logging.error(f"Ошибка загрузки: Репозиторий {REPO_ID} не найден на Hugging Face. Убедитесь, что он создан.") except Exception as e: - logging.error(f"Ошибка при скачивании JSON базы: {e}") - raise + logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}") def periodic_backup(): + """Периодически вызывает upload_db_to_hf.""" + logging.info("Запуск потока периодического резервного копирования.") while True: - upload_db_to_hf() - time.sleep(800) + time.sleep(1800) # Каждые 30 минут + logging.info("Запуск планового резервного копирования...") + with data_lock: # Блокируем на время чтения файла для загрузки + upload_db_to_hf() + +# --- Сериализатор для Decimal --- +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + # Сохраняем как строку для точности + return str(obj) + # Пусть базовый класс обработает остальные типы + return json.JSONEncoder.default(self, obj) + +# --- Преобразователь для Decimal в маршрутах --- +def to_decimal(value_str): + """Безопасно преобразует строку в Decimal.""" + if not value_str: + return Decimal('0.00') + try: + # Заменяем запятую на точку для универсальности ввода + return Decimal(value_str.replace(',', '.')) + except InvalidOperation: + logging.warning(f"Не удалось преобразовать '{value_str}' в Decimal. Возвращено 0.") + return Decimal('0.00') + +# --- Вспомогательные функции для поиска --- +def find_material_by_id(material_id): + """Находит материал по ID.""" + data = load_data() + for material in data.get('materials', []): + if material.get('id') == material_id: + # Преобразуем количество обратно в Decimal при чтении + material['quantity'] = to_decimal(material.get('quantity', '0')) + return material + return None + +def find_cutting_task_by_id(task_id): + """Находит задание на раскрой по ID.""" + data = load_data() + for task in data.get('cutting_tasks', []): + if task.get('id') == task_id: + # Преобразуем числовые поля обратно в Decimal/int + task['cut_items_quantity'] = int(task.get('cut_items_quantity', 0)) + task['fabric_used'] = to_decimal(task.get('fabric_used', '0')) + if 'required_fittings' in task: + for fitting in task['required_fittings']: + fitting['quantity_needed'] = int(fitting.get('quantity_needed', 0)) + return task + return None + +def find_sewing_task_by_id(task_id): + """Находит задание на пошив по ID.""" + data = load_data() + for task in data.get('sewing_tasks', []): + if task.get('id') == task_id: + # Преобразуем числовые поля обратно в Decimal/int + task['sewn_quantity'] = int(task.get('sewn_quantity', 0)) + if 'fittings_consumed' in task: + for fitting in task['fittings_consumed']: + fitting['quantity_used'] = int(fitting.get('quantity_used', 0)) + if 'defects' in task: + for defect in task['defects']: + defect['quantity'] = to_decimal(defect.get('quantity', '0')) if defect.get('type') == 'fabric' else int(defect.get('quantity', 0)) + return task + return None + +# --- Маршруты Flask --- @app.route('/') -def catalog(): +def index(): + """Главная страница, перенаправляет на админку.""" + return redirect(url_for('admin_panel')) + +# 1. Маршрут "Закуп" +@app.route('/procurement', methods=['GET', 'POST']) +def procurement(): + """Страница для добавления закупленных материалов.""" data = load_data() - products = data['products'] - categories = data['categories'] - - catalog_html = ''' - - -
- - -{{ product['description'][:50] }}{% if product['description']|length > 50 %}...{% endif %}
- - -