diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,4 +1,4 @@ - +# Импортируем необходимые библиотеки from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash, send_from_directory import json import os @@ -22,10 +22,10 @@ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER # --- Настройки Hugging Face --- # !!! ВАЖНО: Установите переменные окружения HF_TOKEN_WRITE и HF_TOKEN_READ !!! -# В реальном приложении используйте python-dotenv или системные переменные +# Создайте файл .env или установите системные переменные 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", "Kgshop/Cech") # !!! ЗАМЕНИТЕ НА ВАШ РЕПОЗИТОРИЙ !!! +REPO_ID = "Kgshop/cech" # !!! Обновленный REPO_ID !!! # Убедитесь, что репозиторий существует на Hugging Face как Dataset # --- Настройка логирования --- @@ -106,13 +106,11 @@ def save_data(data): 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(): """Возвращает пустую структуру данных по умолчанию.""" @@ -211,9 +209,31 @@ def find_sewing_task_by_id(task_id): 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)) + if 'defects_reported' in task: # Исправлено с defects на defects_reported + for defect in task['defects_reported']: + # Проверяем тип перед преобразованием + defect_qty_val = defect.get('quantity', '0') + if defect.get('type') == 'fabric': + defect['quantity'] = to_decimal(defect_qty_val) + elif defect.get('type') == 'fittings': + try: + defect['quantity'] = int(Decimal(defect_qty_val)) # Сначала в Decimal для обработки строки, потом в int + except (InvalidOperation, ValueError): + defect['quantity'] = 0 # или другое значение по умолчанию + logging.warning(f"Не удалось преобразовать количество брака фурнитуры '{defect_qty_val}' в int для task {task.get('id')}") + elif defect.get('type') == 'finished_product': + try: + defect['quantity'] = int(Decimal(defect_qty_val)) + except (InvalidOperation, ValueError): + defect['quantity'] = 0 + logging.warning(f"Не удалось преобразовать количество брака готового продукта '{defect_qty_val}' в int для task {task.get('id')}") + else: # На случай других типов или отсутствия + defect['quantity'] = 0 # Безопасное значение по умолчанию + + # Добавим обработку полей qc_packed_quantity и qc_defective_quantity, если они есть + task['qc_packed_quantity'] = int(task.get('qc_packed_quantity', 0)) + task['qc_defective_quantity'] = int(task.get('qc_defective_quantity', 0)) + return task return None @@ -247,6 +267,8 @@ def procurement(): flash("Не добавлено ни одного товара.", "warning") return redirect(url_for('procurement')) + procurement_timestamp = datetime.now().isoformat() # Единое время для всей закупки + for i in range(len(item_names)): name = item_names[i].strip() quantity_str = item_quantities[i] @@ -286,31 +308,36 @@ def procurement(): # Проверяем, существует ли уже материал с таким именем, типом и категорией existing_material = None - for mat in data.get('materials', []): - if mat['name'] == name and mat['type'] == item_type and mat.get('category', 'Без категории') == final_category: + existing_material_index = -1 + for idx, mat in enumerate(data.get('materials', [])): + # Добавим .lower() для нечувствительности к регистру имени при поиске дубликата + if mat['name'].lower() == name.lower() and mat['type'] == item_type and mat.get('category', 'Без категории') == final_category: existing_material = mat + existing_material_index = idx break - if existing_material: + if existing_material and existing_material_index != -1: # Обновляем количество существующего материала current_quantity = to_decimal(existing_material.get('quantity', '0')) - existing_material['quantity'] = current_quantity + quantity - # Можно обновить и другие поля, если нужно (например, items_per_unit) - existing_material['unit'] = unit # Обновим единицу измерения на всякий случай - existing_material['items_per_unit'] = items_per_unit # Обновим - logging.info(f"Обновлено количество материала '{name}' (ID: {existing_material['id']}) на +{quantity}. Новое количество: {existing_material['quantity']}") - + new_total_quantity = current_quantity + quantity + data['materials'][existing_material_index]['quantity'] = str(new_total_quantity) # Сохраняем как строку + # Обновляем другие поля на всякий случай + data['materials'][existing_material_index]['unit'] = unit + data['materials'][existing_material_index]['items_per_unit'] = items_per_unit + data['materials'][existing_material_index]['timestamp_last_updated'] = procurement_timestamp # Добавляем время обновления + logging.info(f"Обновлено количество материала '{name}' (ID: {existing_material['id']}) на +{quantity}. Новое количество: {new_total_quantity}") else: # Добавляем новый материал new_material = { 'id': uuid.uuid4().hex, # Уникальный ID 'name': name, - 'quantity': quantity, # Сохраняем как Decimal + 'quantity': str(quantity), # Сразу сохраняем как строку 'unit': unit, 'items_per_unit': items_per_unit, 'type': item_type, # 'fabric' или 'fittings' 'category': final_category, - 'timestamp_added': datetime.now().isoformat() + 'timestamp_added': procurement_timestamp, + 'timestamp_last_updated': procurement_timestamp } materials_to_add.append(new_material) logging.info(f"Подготовлен к добавлению новый материал: {name} ({quantity} {unit})") @@ -332,8 +359,14 @@ def procurement(): logging.error(f"Ошибка при обработке закупа: {e}", exc_info=True) flash(f"Произошла внутренняя ошибка при обработке закупа: {e}", "danger") - # GET запрос - просто отображаем страницу - return render_template_string(PROCUREMENT_TEMPLATE, categories=categories) + # GET запрос - рендерим шаблон + page_title = "Закуп материалов" + page_content = PROCUREMENT_CONTENT # Используем переменную с контентом + page_scripts = PROCUREMENT_SCRIPTS # Используем переменную со скриптами + final_html = BASE_TEMPLATE.format(title=page_title, content=page_content, scripts=page_scripts) + # Перед��ем нужные переменные в render_template_string + return render_template_string(final_html, categories=categories) + # 2. Маршрут "Раскрой" @app.route('/cutting', methods=['GET', 'POST']) @@ -377,14 +410,14 @@ def cutting(): return redirect(url_for('cutting')) # Проверка наличия достаточного количества ткани - available_fabric = to_decimal(fabric.get('quantity', '0')) + available_fabric = fabric.get('quantity', Decimal('0')) # find_material_by_id уже вернул Decimal if fabric_used > available_fabric: flash(f"Недостаточно ткани '{fabric['name']}'. В наличии: {available_fabric} {fabric['unit']}, требуется: {fabric_used} {fabric['unit']}.", "danger") return redirect(url_for('cutting')) # Сбор информации о необходимой фурнитуре required_fittings_list = [] - total_fittings_cost = Decimal('0.00') # Пример расчета стоимости, если нужно + # total_fittings_cost = Decimal('0.00') # Пример расчета стоимости, если нужно if fitting_ids and fitting_quantities and len(fitting_ids) == len(fitting_quantities): for i in range(len(fitting_ids)): fit_id = fitting_ids[i] @@ -406,11 +439,11 @@ def cutting(): flash(f"Некорректное количество для фурнитуры '{fitting_material['name']}'.", "warning") continue - # Проверка наличия достаточного количества фурнитуры (опционально на этом этапе, строже на пошиве) - # available_fitting = find_material_by_id(fit_id).get('quantity', 0) - # if fit_qty > available_fitting: - # flash(f"Предупреждение: Недостаточно фурнитуры '{fitting_material['name']}'. В наличии: {available_fitting}, требуется: {fit_qty}.", "warning") - # Можно не блокировать, а просто предупредить + # Можно добавить проверку наличия фурнитуры здесь, но строже проверять на этапе пошива + available_fitting_qty = fitting_material.get('quantity', Decimal('0')) + if Decimal(fit_qty) > available_fitting_qty: + flash(f"Предупреждение: Недостаточно фурнитуры '{fitting_material['name']}'. В наличии: {available_fitting_qty}, требуется: {fit_qty}.", "warning") + # Не блокируем, просто предупреждаем required_fittings_list.append({ 'fitting_id': fit_id, @@ -426,21 +459,28 @@ def cutting(): 'fabric_name': fabric['name'], 'fabric_unit': fabric['unit'], 'cut_items_quantity': cut_items_quantity, - 'fabric_used': fabric_used, # Сохраняем как Decimal + 'fabric_used': str(fabric_used), # Сохраняем как строку 'required_fittings': required_fittings_list, - 'status': 'pending', # 'pending', 'in_progress' (sewing), 'completed' (sewn) + 'status': 'pending', # 'pending', 'completed' (когда отправлено на пошив) 'timestamp_created': datetime.now().isoformat(), - 'timestamp_completed': None + 'timestamp_completed': None # Заполнится при создании задания на пошив } # Обновление количества ткани в базе - fabric['quantity'] = available_fabric - fabric_used + new_fabric_quantity = available_fabric - fabric_used # Найдем индекс ткани в списке и обновим её + updated = False for i, mat in enumerate(data.get('materials', [])): if mat.get('id') == fabric_id: - # Убедимся что сохраняем как строку в JSON - data['materials'][i]['quantity'] = str(fabric['quantity']) + data['materials'][i]['quantity'] = str(new_fabric_quantity) + data['materials'][i]['timestamp_last_updated'] = new_cutting_task['timestamp_created'] + updated = True break + if not updated: + # Этого не должно произойти, если find_material_by_id сработал, но на всякий случай + flash(f"Критическая ошибка: не удалось обновить количество ткани {fabric['name']} в базе данных.", "danger") + return redirect(url_for('cutting')) + # Добавление задания в список if 'cutting_tasks' not in data: @@ -469,7 +509,11 @@ def cutting(): f_copy['quantity'] = to_decimal(f_copy.get('quantity', '0')) fittings_dec.append(f_copy) - return render_template_string(CUTTING_TEMPLATE, fabrics=fabrics_dec, fittings=fittings_dec) + page_title = "Раскрой ткани" + page_content = CUTTING_CONTENT + page_scripts = CUTTING_SCRIPTS + final_html = BASE_TEMPLATE.format(title=page_title, content=page_content, scripts=page_scripts) + return render_template_string(final_html, fabrics=fabrics_dec, fittings=fittings_dec) # 3. Маршрут "Пошив" @@ -493,19 +537,20 @@ def sewing(): flash("Выберите задание, укажите название изделия и количество сшитых.", "danger") return redirect(url_for('sewing')) - cutting_task = find_cutting_task_by_id(cutting_task_id) + cutting_task = find_cutting_task_by_id(cutting_task_id) # Вернет данные с преобразованными числами if not cutting_task or cutting_task.get('status') != 'pending': - flash("Выбранное задание на раскрой не найдено или уже обрабатывается/завершено.", "danger") + flash("Выбранное задание на раскрой не найдено или уже отправлено на пошив.", "danger") return redirect(url_for('sewing')) try: sewn_quantity = int(sewn_quantity_str) + cut_items_quantity_task = cutting_task.get('cut_items_quantity', 0) if sewn_quantity <= 0: flash("Количество сшитых изделий должно быть положительным.", "danger") return redirect(url_for('sewing')) # Нельзя сшить больше, чем было раскроено - if sewn_quantity > cutting_task.get('cut_items_quantity', 0): - flash(f"Нельзя сшить больше изделий ({sewn_quantity}), чем было раскроено ({cutting_task.get('cut_items_quantity', 0)}).", "danger") + if sewn_quantity > cut_items_quantity_task: + flash(f"Нельзя сшить больше изделий ({sewn_quantity}), чем было раскроено ({cut_items_quantity_task}).", "danger") return redirect(url_for('sewing')) except ValueError: @@ -515,21 +560,26 @@ def sewing(): # Обработка списания фурнитуры и брака fittings_consumed_list = [] defect_log_list = [] - fittings_to_update = {} # Словарь {fitting_id: quantity_to_deduct} + materials_to_update = {} # Словарь {material_id: quantity_to_deduct (Decimal)} # 1. Списание необходимой фурнитуры required_fittings = cutting_task.get('required_fittings', []) can_sew = True + current_time = datetime.now().isoformat() + for req_fitting in required_fittings: fitting_id = req_fitting['fitting_id'] - needed_total = req_fitting['quantity_needed'] # Сколько нужно было на весь крой + needed_total_on_cut = req_fitting['quantity_needed'] # Сколько нужно было на весь крой + # Пропорционально рассчитываем, сколько нужно на фактически сшитое количество - needed_for_sewn = 0 - if cutting_task.get('cut_items_quantity', 0) > 0: - needed_for_sewn = round( (Decimal(needed_total) / Decimal(cutting_task['cut_items_quantity'])) * Decimal(sewn_quantity) ) - needed_for_sewn = int(needed_for_sewn) # Округляем до целого + needed_for_this_sewing = Decimal('0') + if cut_items_quantity_task > 0: + # Используем Decimal для точности расчета доли + needed_for_this_sewing = (Decimal(needed_total_on_cut) / Decimal(cut_items_quantity_task)) * Decimal(sewn_quantity) + # Округляем до целого, т.к. фурнитура обычно целая + needed_for_this_sewing = int(needed_for_this_sewing.to_integral_value(rounding='ROUND_HALF_UP')) - if needed_for_sewn <= 0: continue # Если ничего не нужно, пропускаем + if needed_for_this_sewing <= 0: continue # Если ничего не нужно, пропускаем fitting_material = find_material_by_id(fitting_id) if not fitting_material: @@ -537,18 +587,18 @@ def sewing(): can_sew = False break # Критическая ошибка - available_fitting = to_decimal(fitting_material.get('quantity', '0')) - if available_fitting < needed_for_sewn: - flash(f"Недостаточно фурнитуры '{fitting_material['name']}'. В наличии: {available_fitting}, требуется: {needed_for_sewn}.", "danger") + available_fitting = fitting_material.get('quantity', Decimal('0')) # Already Decimal + if available_fitting < Decimal(needed_for_this_sewing): + flash(f"Недостаточно фурнитуры '{fitting_material['name']}'. В наличии: {available_fitting}, требуется: {needed_for_this_sewing}.", "danger") can_sew = False break # Не можем шить без фурнитуры - # Добавляем в список для списания - fittings_to_update[fitting_id] = fittings_to_update.get(fitting_id, 0) + needed_for_sewn + # Добавляем в список для списания (используем Decimal для внутреннего учета) + materials_to_update[fitting_id] = materials_to_update.get(fitting_id, Decimal('0')) + Decimal(needed_for_this_sewing) fittings_consumed_list.append({ 'fitting_id': fitting_id, 'fitting_name': fitting_material['name'], - 'quantity_used': needed_for_sewn + 'quantity_used': needed_for_this_sewing # Сколько ушло именно на этот пошив }) if not can_sew: @@ -562,7 +612,7 @@ def sewing(): if not def_mat_id or not def_qty_str: continue - defect_material = find_material_by_id(def_mat_id) + defect_material = find_material_by_id(def_mat_id) # Already Decimal quantity if not defect_material: flash(f"Материал для брака с ID {def_mat_id} не найден.", "warning") continue @@ -570,9 +620,10 @@ def sewing(): try: # Количество брака может быть дробным для ткани if defect_material['type'] == 'fabric': - def_qty = to_decimal(def_qty_str) + def_qty = to_decimal(def_qty_str) # Преобразуем введенное значение else: # Целое для фурнитуры - def_qty = Decimal(int(def_qty_str)) # Временно Decimal для сравнения + def_qty = Decimal(int(to_decimal(def_qty_str))) # Преобразуем и округляем до целого Decimal + if def_qty <= 0: flash(f"Количество брака для '{defect_material['name']}' должно быть положительным.", "warning") continue @@ -580,59 +631,66 @@ def sewing(): flash(f"Некорректное количество брака для '{defect_material['name']}'.", "warning") continue - available_defect_material = to_decimal(defect_material.get('quantity', '0')) + available_defect_material = defect_material.get('quantity', Decimal('0')) - # Если это фурнитура, которую мы уже списываем для пошива, - # нужно учесть это при проверке доступности для брака - already_deducted = Decimal(fittings_to_update.get(def_mat_id, 0)) - effective_available = available_defect_material - already_deducted + # Учитываем уже запланированное к списанию количество этого материала + already_planned_deduction = materials_to_update.get(def_mat_id, Decimal('0')) + effective_available = available_defect_material - already_planned_deduction if effective_available < def_qty: flash(f"Недостаточно материала '{defect_material['name']}' для списания в брак ({def_qty} {defect_material['unit']}). Доступно с учетом расхода: {effective_available}.", "danger") can_sew = False # Не мо��ем списать в брак больше, чем есть break - # Добавляем в лог брака и в словарь для списания - unit_str = defect_material['unit'] if defect_material['type'] == 'fabric' else 'шт' + # Добавляем количество брака к общему количеству для списания этого материала + materials_to_update[def_mat_id] = materials_to_update.get(def_mat_id, Decimal('0')) + def_qty + + # Добавляем в лог брака + unit_str = defect_material['unit'] + final_defect_qty_for_log = def_qty # Decimal для ткани + if defect_material['type'] == 'fittings': + final_defect_qty_for_log = int(def_qty) # Int для фурнитуры в логе + defect_log_entry = { 'log_id': uuid.uuid4().hex, 'material_id': def_mat_id, 'material_name': defect_material['name'], - 'quantity': def_qty, # Decimal или int в зависимости от типа + 'quantity': final_defect_qty_for_log, # Сохраняем соответствующий тип 'unit': unit_str, 'type': defect_material['type'], 'stage': 'sewing', 'reason': 'Брак при пошиве', # Можно добавить поле для причины 'sewing_task_id': None, # Добавим ID после создания задания на пошив - 'timestamp': datetime.now().isoformat() + 'timestamp': current_time } - # Преобразуем обратно в int для фурнитуры перед добавлением в лог - if defect_material['type'] == 'fittings': - defect_log_entry['quantity'] = int(def_qty) - defect_log_list.append(defect_log_entry) - - # Добавляем количество брака к общему количеству для списания - fittings_to_update[def_mat_id] = fittings_to_update.get(def_mat_id, 0) + int(def_qty) if defect_material['type'] == 'fittings' else to_decimal(fittings_to_update.get(def_mat_id, '0')) + def_qty - - if not can_sew: return redirect(url_for('sewing')) # Прерываем, если не хватило на брак - # 3. Обновляем количество фурнитуры и ткани (из брака) в базе - for mat_id, qty_to_deduct in fittings_to_update.items(): - material_to_update = find_material_by_id(mat_id) - current_qty = to_decimal(material_to_update.get('quantity', '0')) - new_quantity = current_qty - to_decimal(str(qty_to_deduct)) # Приводим к Decimal для вычитания - - # Найдем индекс материала и обновим его - for i, mat in enumerate(data.get('materials', [])): - if mat.get('id') == mat_id: - # Убедимся что сохраняем как строку в JSON - data['materials'][i]['quantity'] = str(new_quantity) - logging.info(f"Списано материала '{material_to_update['name']}' (ID: {mat_id}): {qty_to_deduct}. Остаток: {new_quantity}") - break + # 3. Обновляем количество всех материалов, которые нужно списать + materials_list = data.get('materials', []) + for mat_id, qty_to_deduct in materials_to_update.items(): + updated = False + for i, mat in enumerate(materials_list): + if mat.get('id') == mat_id: + current_qty_dec = to_decimal(mat.get('quantity', '0')) + new_quantity_dec = current_qty_dec - qty_to_deduct + # Проверка на отрицательный остаток (на всякий случай) + if new_quantity_dec < 0: + logging.error(f"Ошибка расчета: отрицательный остаток для материала {mat_id}. Остаток: {new_quantity_dec}. Списание: {qty_to_deduct}") + new_quantity_dec = Decimal('0.00') # Не уходим в минус + + materials_list[i]['quantity'] = str(new_quantity_dec) # Сохраняем как строку + materials_list[i]['timestamp_last_updated'] = current_time + logging.info(f"Списано материала '{mat['name']}' (ID: {mat_id}): {qty_to_deduct}. Остаток: {new_quantity_dec}") + updated = True + break + if not updated: + logging.error(f"Не удалось найти материал с ID {mat_id} для обновления остатка при пошиве.") + # Решить, что делать - прерывать или продолжать с ошибкой? + flash(f"Критическая ошибка: Не удалось найти материал ID {mat_id} для списания.", "danger") + return redirect(url_for('sewing')) # 4. Создание задания на пошив new_sewing_task = { @@ -643,31 +701,45 @@ def sewing(): 'fabric_id': cutting_task['fabric_id'], # Сохраняем для справки 'fabric_name': cutting_task['fabric_name'], 'fittings_consumed': fittings_consumed_list, # Фактически использовано на пошив - 'defects_reported': defect_log_list, # Залогированный брак - 'status': 'pending_qc', # 'pending_qc', 'in_qc', 'completed' (packed) - 'timestamp_created': datetime.now().isoformat(), - 'timestamp_completed': None + 'defects_reported': defect_log_list, # Залогированный брак (уже с правильными типами) + 'status': 'pending_qc', # 'pending_qc', 'completed' (packed) + 'timestamp_created': current_time, + 'timestamp_completed': None, # Заполнится при ОТК + 'qc_packed_quantity': 0, # Инициализация полей ОТК + 'qc_defective_quantity': 0 } # Присваиваем ID задания на пошив записям в логе брака for defect in defect_log_list: defect['sewing_task_id'] = new_sewing_task['id'] + # Преобразуем Decimal обратно в строку для JSON, если это ткань + if defect['type'] == 'fabric': + defect['quantity'] = str(defect['quantity']) + - # 5. Обновление статуса задания на раскрой - cutting_task['status'] = 'completed' # Считаем раскрой завершенным - cutting_task['timestamp_completed'] = datetime.now().isoformat() - # Найдем индекс задания на раскрой и обновим его + # 5. Обновление статуса задания на раскрой (или частичное обновление?) + # Пока считаем, что отправка на пошив завершает задание на раскрой целиком. + task_updated = False for i, task in enumerate(data.get('cutting_tasks', [])): if task.get('id') == cutting_task_id: - data['cutting_tasks'][i] = cutting_task + task['status'] = 'completed' # Считаем раскрой завершенным + task['timestamp_completed'] = current_time + task_updated = True break + if not task_updated: + logging.error(f"Не удалось найти задание на раскрой ID {cutting_task_id} для обновления статуса.") + # Решить, что делать # 6. Добавление задания на пошив и записей о браке в базу if 'sewing_tasks' not in data: data['sewing_tasks'] = [] data['sewing_tasks'].append(new_sewing_task) - if 'defect_log' not in data: data['defect_log'] = [] - data['defect_log'].extend(defect_log_list) + if defect_log_list: + if 'defect_log' not in data: data['defect_log'] = [] + data['defect_log'].extend(defect_log_list) + + # Обновляем основной список материалов + data['materials'] = materials_list save_data(data) flash(f"Успешно зарегистрирован пошив {sewn_quantity} ед. изделия '{sewn_product_name}'. Задание отправлено на ОТК.", "success") @@ -683,27 +755,34 @@ def sewing(): # GET запрос # Преобразуем данные для шаблона tasks_for_template = [] + all_materials_dict = {m['id']: m for m in data.get('materials', [])} # Словарь для быстрого поиска остатков + for task in pending_cutting_tasks: task_copy = task.copy() - task_copy['fabric_used'] = to_decimal(task_copy.get('fabric_used', '0')) - # Получаем текущие остатки фурнитуры для отображения + task_copy['fabric_used_decimal'] = to_decimal(task_copy.get('fabric_used', '0')) # Для возможного отображения + # Получаем текущие остатки фурнитуры для отображения в деталях if 'required_fittings' in task_copy: for fitting in task_copy['required_fittings']: - mat = find_material_by_id(fitting['fitting_id']) - fitting['available_quantity'] = to_decimal(mat.get('quantity', '0')) if mat else Decimal('0') + mat_data = all_materials_dict.get(fitting['fitting_id']) + fitting['available_quantity'] = to_decimal(mat_data.get('quantity', '0')) if mat_data else Decimal('0') + fitting['unit'] = mat_data.get('unit', 'шт') if mat_data else 'шт' tasks_for_template.append(task_copy) - # Получаем все материалы для выбора брака + # Получаем все материалы для выбора брака (с преобразованными количествами) all_materials_dec = [] - for m in data.get('materials', []): - m_copy = m.copy() - m_copy['quantity'] = to_decimal(m_copy.get('quantity', '0')) + for m_id, m_data in all_materials_dict.items(): + m_copy = m_data.copy() + m_copy['quantity_decimal'] = to_decimal(m_copy.get('quantity', '0')) all_materials_dec.append(m_copy) - return render_template_string(SEWING_TEMPLATE, + page_title = "Пошив изделий" + page_content = SEWING_CONTENT + page_scripts = SEWING_SCRIPTS + final_html = BASE_TEMPLATE.format(title=page_title, content=page_content, scripts=page_scripts) + return render_template_string(final_html, cutting_tasks=tasks_for_template, - all_materials=all_materials_dec) + all_materials=all_materials_dec) # Передаем материалы с quantity_decimal # 4. Маршрут "ОТК и Упаковка" @app.route('/qc_packing', methods=['GET', 'POST']) @@ -718,19 +797,22 @@ def qc_packing(): sewing_task_id = request.form.get('sewing_task_id') quantity_packed_str = request.form.get('quantity_packed') quantity_defective_str = request.form.get('quantity_defective', '0') # Брак на этапе ОТК - defect_reason = request.form.get('defect_reason', 'Брак при ОТК/упаковке') + defect_reason = request.form.get('defect_reason', 'Брак при ОТК/упаковке').strip() if not sewing_task_id or not quantity_packed_str: flash("Выберите задание и укажите количество упакованных изделий.", "danger") return redirect(url_for('qc_packing')) - sewing_task = find_sewing_task_by_id(sewing_task_id) + sewing_task = find_sewing_task_by_id(sewing_task_id) # Уже с int количествами if not sewing_task or sewing_task.get('status') != 'pending_qc': flash("Выбранное задание на пошив не найдено или уже обработано.", "danger") return redirect(url_for('qc_packing')) try: - quantity_packed = int(quantity_packed_str) + quantity_packed = 0 + if quantity_packed_str: + quantity_packed = int(quantity_packed_str) + quantity_defective = 0 if quantity_defective_str: quantity_defective = int(quantity_defective_str) @@ -740,20 +822,23 @@ def qc_packing(): return redirect(url_for('qc_packing')) total_processed = quantity_packed + quantity_defective - sewn_quantity = sewing_task.get('sewn_quantity', 0) + sewn_quantity_task = sewing_task.get('sewn_quantity', 0) if total_processed == 0: - flash("Укажите количество упакованных или бракованных изделий.", "warning") + flash("Укажите количество упакованных или бракованных изделий (хотя бы одно должно быть больше нуля).", "warning") return redirect(url_for('qc_packing')) - if total_processed > sewn_quantity: - flash(f"Общее количество обработанных изделий ({total_processed}) не может превышать количество сшитых ({sewn_quantity}).", "danger") + # Проверяем, не пытаются ли обработать ��ольше, чем было сшито ИЗНАЧАЛЬНО + if total_processed > sewn_quantity_task: + flash(f"Ошибка: Сумма упакованных ({quantity_packed}) и брака ({quantity_defective}) = {total_processed}, что больше, чем было сшито ({sewn_quantity_task}).", "danger") return redirect(url_for('qc_packing')) except ValueError: flash("Некорректное количество изделий.", "danger") return redirect(url_for('qc_packing')) + current_time = datetime.now().isoformat() + # Создание записи о готовой продукции if quantity_packed > 0: packed_item = { @@ -761,7 +846,7 @@ def qc_packing(): 'sewing_task_id': sewing_task_id, 'product_name': sewing_task['product_name'], 'quantity': quantity_packed, - 'timestamp_packed': datetime.now().isoformat() + 'timestamp_packed': current_time } if 'qc_packing_items' not in data: data['qc_packing_items'] = [] data['qc_packing_items'].append(packed_item) @@ -773,34 +858,35 @@ def qc_packing(): 'log_id': uuid.uuid4().hex, 'material_id': None, # Брак готового изделия, не материала 'material_name': sewing_task['product_name'] + " (готовое изделие)", - 'quantity': quantity_defective, + 'quantity': quantity_defective, # int 'unit': 'шт', 'type': 'finished_product', 'stage': 'qc_packing', - 'reason': defect_reason, + 'reason': defect_reason if defect_reason else 'Брак при ОТК/упаковке', 'sewing_task_id': sewing_task_id, - 'timestamp': datetime.now().isoformat() + 'timestamp': current_time } if 'defect_log' not in data: data['defect_log'] = [] data['defect_log'].append(defect_log_entry) logging.info(f"Зарегистрирован брак {quantity_defective} ед. готового изделия '{sewing_task['product_name']}' на этапе ОТК.") - # Обновление статуса задания на пошив - # Можно сделать статус 'partially_completed', если не все обработано, - # но для простоты считаем завершенным, если хоть что-то обработано. - sewing_task['status'] = 'completed' - sewing_task['timestamp_completed'] = datetime.now().isoformat() - # Найдем индекс задания на пошив и обновим его + # Обновление статуса и данных в задании на пошив + task_updated = False for i, task in enumerate(data.get('sewing_tasks', [])): if task.get('id') == sewing_task_id: - # Добавим информацию о результате ОТК в само задание - task['qc_packed_quantity'] = quantity_packed - task['qc_defective_quantity'] = quantity_defective - task['status'] = 'completed' - task['timestamp_completed'] = sewing_task['timestamp_completed'] + # Добавляем информацию о результате ОТК в само задание + task['qc_packed_quantity'] = task.get('qc_packed_quantity', 0) + quantity_packed # Накапливаем, если частичная приемка + task['qc_defective_quantity'] = task.get('qc_defective_quantity', 0) + quantity_defective + task['status'] = 'completed' # Считаем завершенным после обработки + task['timestamp_completed'] = current_time # Время последнего ОТК + task_updated = True break + if not task_updated: + logging.error(f"Не удалось найти задание на пошив ID {sewing_task_id} для обновления данных ОТК.") + # Решить, что делать + save_data(data) flash(f"ОТК и упаковка для задания '{sewing_task['product_name']}' зарегистрированы: упаковано {quantity_packed}, брак {quantity_defective}.", "success") return redirect(url_for('qc_packing')) @@ -810,7 +896,12 @@ def qc_packing(): flash(f"Произошла внутренняя ошибка при обработке ОТК и упаковки: {e}", "danger") # GET запрос - return render_template_string(QC_PACKING_TEMPLATE, sewing_tasks=pending_sewing_tasks) + page_title = "ОТК и Упаковка" + page_content = QC_PACKING_CONTENT + page_scripts = QC_PACKING_SCRIPTS + final_html = BASE_TEMPLATE.format(title=page_title, content=page_content, scripts=page_scripts) + # Передаем задачи, ожидающие ОТК, в шаблон + return render_template_string(final_html, sewing_tasks=pending_sewing_tasks) # 5. Маршрут "Админ-панель" @@ -827,33 +918,98 @@ def admin_panel(): defect_log = data.get('defect_log', []) categories = data.get('categories', []) - # Преобразуем количества в Decimal для корректного отображения - materials_dec = [] + # Преобразуем количества в Decimal/int для корректного отображения и расчетов + materials_view = [] for m in materials: m_copy = m.copy() - m_copy['quantity'] = to_decimal(m_copy.get('quantity', '0')) - materials_dec.append(m_copy) + m_copy['quantity_dec'] = to_decimal(m_copy.get('quantity', '0')) + m_copy['items_per_unit_int'] = int(m_copy.get('items_per_unit', 0)) + materials_view.append(m_copy) - cutting_tasks_dec = [] + cutting_tasks_view = [] for t in cutting_tasks: t_copy = t.copy() - t_copy['fabric_used'] = to_decimal(t_copy.get('fabric_used', '0')) - cutting_tasks_dec.append(t_copy) - - # Можно добавить расчеты итогов, если нужно - total_packed_count = sum(item.get('quantity', 0) for item in packed_items) - total_defect_items = sum(item.get('quantity', 0) for item in defect_log) # Суммируем весь брак - + t_copy['fabric_used_dec'] = to_decimal(t_copy.get('fabric_used', '0')) + t_copy['cut_items_quantity_int'] = int(t_copy.get('cut_items_quantity', 0)) + # Преобразуем кол-во в фиттингах + if 'required_fittings' in t_copy: + for fit in t_copy['required_fittings']: + fit['quantity_needed_int'] = int(fit.get('quantity_needed', 0)) + cutting_tasks_view.append(t_copy) + + sewing_tasks_view = [] + for t in sewing_tasks: + # Используем find_sewing_task_by_id, чтобы получить уже преобразованные числа + task_data = find_sewing_task_by_id(t['id']) + if task_data: # Убедимся, что задача найдена + sewing_tasks_view.append(task_data) + else: + logging.warning(f"Задача на пошив с ID {t['id']} не найдена при подготовке для админки.") + # Можно добавить копию исходной задачи t, но числа могут быть строками + sewing_tasks_view.append(t.copy()) + + + packed_items_view = [] + total_packed_count = 0 + for item in packed_items: + item_copy = item.copy() + item_quantity = int(item_copy.get('quantity', 0)) + item_copy['quantity_int'] = item_quantity + total_packed_count += item_quantity + packed_items_view.append(item_copy) + + defect_log_view = [] + total_defect_items = 0 # Будем считать штуки, метры отдельно + total_defect_fabric_m = Decimal('0.00') + total_defect_fittings_pcs = 0 + total_defect_finished_pcs = 0 + + for defect in defect_log: + defect_copy = defect.copy() + qty = defect_copy.get('quantity', 0) + unit = defect_copy.get('unit', '') + dtype = defect_copy.get('type', '') + + if dtype == 'fabric': + qty_dec = to_decimal(str(qty)) # Преобразуем, если строка + defect_copy['quantity_view'] = f"{qty_dec}".replace('.', ',') # Формат для вывода + total_defect_fabric_m += qty_dec + elif dtype == 'fittings': + qty_int = int(Decimal(str(qty))) # Преобразуем безопасно + defect_copy['quantity_view'] = f"{qty_int}" + total_defect_fittings_pcs += qty_int + elif dtype == 'finished_product': + qty_int = int(Decimal(str(qty))) + defect_copy['quantity_view'] = f"{qty_int}" + total_defect_finished_pcs += qty_int + else: + defect_copy['quantity_view'] = str(qty) # Как есть + + defect_log_view.append(defect_copy) + + + page_title = "Админ-панель" + page_content = ADMIN_CONTENT + page_scripts = ADMIN_SCRIPTS # Если есть специфичные скрипты для админки + final_html = BASE_TEMPLATE.format(title=page_title, content=page_content, scripts=page_scripts) + + # Передаем подготовленные данные return render_template_string( - ADMIN_TEMPLATE, - materials=materials_dec, - cutting_tasks=cutting_tasks_dec, - sewing_tasks=sewing_tasks, # В sewing_tasks количество целочисленное - packed_items=packed_items, # В packed_items количество целочисленное - defect_log=defect_log, # Количество может быть разным + final_html, + materials=materials_view, + cutting_tasks=cutting_tasks_view, + sewing_tasks=sewing_tasks_view, + packed_items=packed_items_view, + defect_log=defect_log_view, categories=categories, + # Передаем итоги для сводки + materials_count=len(materials_view), + pending_cutting_count=len([t for t in cutting_tasks_view if t.get('status') == 'pending']), + pending_qc_count=len([t for t in sewing_tasks_view if t.get('status') == 'pending_qc']), total_packed_count=total_packed_count, - total_defect_items=total_defect_items + total_defect_fabric_m=str(total_defect_fabric_m).replace('.', ','), # Строка для вывода + total_defect_fittings_pcs=total_defect_fittings_pcs, + total_defect_finished_pcs=total_defect_finished_pcs ) # Маршруты для управления категориями в админке @@ -863,9 +1019,9 @@ def add_category(): categories = data.get('categories', []) new_category = request.form.get('new_category_name', '').strip() - if new_category and new_category not in categories: + if new_category and new_category.lower() not in [c.lower() for c in categories]: # Проверка без учета регистра categories.append(new_category) - data['categories'] = sorted(list(set(categories))) + data['categories'] = sorted(list(set(categories)), key=str.lower) # Сортировка без учета регистра save_data(data) flash(f"Категория '{new_category}' успешно добавлена.", "success") elif not new_category: @@ -881,19 +1037,36 @@ def delete_category(): categories = data.get('categories', []) category_to_delete = request.form.get('category_to_delete') - if category_to_delete and category_to_delete in categories: - categories.remove(category_to_delete) - data['categories'] = sorted(categories) - # Опционально: изменить категорию у материалов + if category_to_delete == 'Без категории': + flash("Нельзя удалить базовую категорию 'Без категории'.", "danger") + return redirect(url_for('admin_panel')) + + + # Ищем категорию без учета регистра для удаления, но сохраняем оригинальное название для сообщений + original_category_name = None + category_found = False + for cat in categories: + if cat.lower() == category_to_delete.lower(): + original_category_name = cat + category_found = True + break + + if category_found and original_category_name: + categories.remove(original_category_name) + data['categories'] = sorted(categories, key=str.lower) + + # Обновляем категорию у материалов materials = data.get('materials', []) updated_count = 0 for mat in materials: - if mat.get('category') == category_to_delete: - mat['category'] = 'Без категории' # Или другая категория по умолчанию + # Сравниваем без учета регистра + if mat.get('category', 'Без категории').lower() == original_category_name.lower(): + mat['category'] = 'Без категории' + mat['timestamp_last_updated'] = datetime.now().isoformat() updated_count += 1 save_data(data) - flash(f"Категория '{category_to_delete}' удалена.", "success") + flash(f"Категория '{original_category_name}' удалена.", "success") if updated_count > 0: flash(f"{updated_count} материалов перенесены в категорию 'Без категории'.", "info") elif not category_to_delete: @@ -904,144 +1077,216 @@ def delete_category(): return redirect(url_for('admin_panel')) -# Маршруты для Hugging Face (как в примере, но адаптированные) +# Маршруты для Hugging Face @app.route('/backup', methods=['POST']) def backup_hf(): """Принудительно создает резервную копию на HF.""" try: - # save_data уже вызывает upload_db_to_hf, но можно вызвать явно для уверенности - # load_data() # Убедимся, что работаем с актуа��ьными данными перед сохранением - data = load_data() - save_data(data) # Сохранит локально и загрузит на HF - flash("Резервная копия успешно создана и загружена на Hugging Face.", "success") + logging.info("Запуск ручного резервного копирования на Hugging Face...") + # Убедимся, что локальный файл существует перед загрузкой + if os.path.exists(DATA_FILE): + upload_db_to_hf() # Просто загружаем текущий локальный файл + flash("Резервная копия успешно загружена на Hugging Face.", "success") + else: + flash("Локальный файл данных не найден. Нечего загружать.", "warning") except Exception as e: logging.error(f"Ошибка при ручном резервном копировании: {e}") flash(f"Ошибка при создании резервной копии: {e}", "danger") - return redirect(url_for('admin_panel')) # Или куда удобнее + return redirect(url_for('admin_panel')) @app.route('/download', methods=['GET']) def download_hf(): """Принудительно скачивает базу данных с HF (перезаписывает локальную).""" # !!! ОСТОРОЖНО: Эта операция перезапишет локальные несинхронизированные изменения !!! try: - # Просто вызываем load_data, он сам скачает - load_data() + logging.info("Запуск ручного скачивания базы данных с Hugging Face...") + # load_data() уже скачивает, но можно вызвать явно hf_hub_download для надежности + 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 # Принудительно скачать + ) flash(f"База данных успешно скачана с Hugging Face. Локальный файл {DATA_FILE} обновлен.", "success") + except RepositoryNotFoundError: + flash(f"Репозиторий {REPO_ID} не найден на Hugging Face.", "danger") + except HfHubHTTPError as e: + if e.response.status_code == 404: + flash(f"Файл {DATA_FILE} не найден в репозитории {REPO_ID}.", "danger") + else: + flash(f"Ошибка HTTP при скачивании из Hugging Face: {e}", "danger") + logging.error(f"Ошибка HTTP при ручном скачивании базы данных: {e}") except Exception as e: logging.error(f"Ошибка при ручном скачивании базы данных: {e}") flash(f"Ошибка при скачивании базы данных: {e}", "danger") return redirect(url_for('admin_panel')) -# --- HTML Шаблоны --- +# --- HTML Шаблоны (как строки Python) --- -# Базовый шаблон для структуры страниц +# Базовый шаблон с плейсхолдерами BASE_TEMPLATE = """
-