Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -86,6 +86,11 @@ def load_data():
|
|
| 86 |
if key not in data:
|
| 87 |
logging.warning(f"В {DATA_FILE} отсутствует ключ '{key}'. Инициализация значением по умолчанию.")
|
| 88 |
data[key] = default_data[key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# Дополнительно проверяем config
|
| 90 |
if 'config' not in data or not isinstance(data['config'], dict):
|
| 91 |
logging.warning(f"В {DATA_FILE} отсутствует или некорректен ключ 'config'. Инициализация значением по умолчанию.")
|
|
@@ -96,13 +101,20 @@ def load_data():
|
|
| 96 |
if config_key not in data['config']:
|
| 97 |
logging.warning(f"В {DATA_FILE}['config'] отсутствует ключ '{config_key}'. Инициализация значением по умолчанию.")
|
| 98 |
data['config'][config_key] = default_value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
return data
|
| 100 |
except FileNotFoundError:
|
| 101 |
logging.warning(f"Локальный файл {DATA_FILE} не найден. Инициализация пустой структурой.")
|
| 102 |
return initialize_data_structure()
|
| 103 |
except json.JSONDecodeError:
|
| 104 |
logging.error(f"Ошибка декодирования JSON в файле {DATA_FILE}. Инициализация пустой структурой.")
|
| 105 |
-
# Попытка создать бэкап поврежденного файла
|
| 106 |
try:
|
| 107 |
bad_file_path = f"{DATA_FILE}.{get_current_time().strftime('%Y%m%d%H%M%S')}.bad"
|
| 108 |
os.rename(DATA_FILE, bad_file_path)
|
|
@@ -111,7 +123,7 @@ def load_data():
|
|
| 111 |
logging.error(f"Не удалось создать бэкап поврежденного файла {DATA_FILE}: {backup_err}")
|
| 112 |
return initialize_data_structure()
|
| 113 |
except Exception as e:
|
| 114 |
-
logging.error(f"Неизвестная ошибка при загрузке локальных основных данных: {e}")
|
| 115 |
return initialize_data_structure()
|
| 116 |
|
| 117 |
def save_data(data):
|
|
@@ -124,7 +136,7 @@ def save_data(data):
|
|
| 124 |
os.replace(temp_file, DATA_FILE)
|
| 125 |
logging.info(f"Основные данные успешно сохранены в локальный файл {DATA_FILE}.")
|
| 126 |
except Exception as e:
|
| 127 |
-
logging.error(f"Критическая ошибка при сохранении основных данных: {e}")
|
| 128 |
if os.path.exists(temp_file):
|
| 129 |
try: os.remove(temp_file)
|
| 130 |
except OSError as rm_err: logging.error(f"Не удалось удалить временный файл {temp_file}: {rm_err}")
|
|
@@ -180,7 +192,6 @@ def load_client_data():
|
|
| 180 |
except FileNotFoundError: logging.warning(f"Локальный файл {CLIENT_DATA_FILE} не найден. Инициализация пустым списком."); return []
|
| 181 |
except json.JSONDecodeError:
|
| 182 |
logging.error(f"Ошибка декодирования JSON в файле {CLIENT_DATA_FILE}. Инициализация пустым списком.")
|
| 183 |
-
# Попытка создать бэкап поврежденного файла
|
| 184 |
try:
|
| 185 |
bad_file_path = f"{CLIENT_DATA_FILE}.{get_current_time().strftime('%Y%m%d%H%M%S')}.bad"
|
| 186 |
os.rename(CLIENT_DATA_FILE, bad_file_path)
|
|
@@ -188,7 +199,7 @@ def load_client_data():
|
|
| 188 |
except Exception as backup_err:
|
| 189 |
logging.error(f"Не удалось создать бэкап поврежденного файла {CLIENT_DATA_FILE}: {backup_err}")
|
| 190 |
return []
|
| 191 |
-
except Exception as e: logging.error(f"Неизвестная ошибка при загрузке локальных данных клиентов: {e}"); return []
|
| 192 |
|
| 193 |
def save_client_data(clients):
|
| 194 |
"""Сохраняет данные клиентов в JSON файл."""
|
|
@@ -221,7 +232,7 @@ def save_client_data(clients):
|
|
| 221 |
os.replace(temp_file, CLIENT_DATA_FILE)
|
| 222 |
logging.info(f"Данные клиентов успешно сохранены в локальный файл {CLIENT_DATA_FILE}.")
|
| 223 |
except Exception as e:
|
| 224 |
-
logging.error(f"Критическая ошибка при сохранении данных клиентов: {e}")
|
| 225 |
if os.path.exists(temp_file):
|
| 226 |
try: os.remove(temp_file)
|
| 227 |
except OSError as rm_err: logging.error(f"Не удалось удалить временный файл {temp_file}: {rm_err}")
|
|
@@ -399,10 +410,21 @@ def find_item_by_id(item_id, item_list_name):
|
|
| 399 |
|
| 400 |
# Применяем преобразования
|
| 401 |
for field in decimal_fields:
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
for field in int_fields:
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
except Exception as conversion_error:
|
| 408 |
logging.error(f"Ошибка преобразования типов для {item_list_name} ID {item_id}: {conversion_error}", exc_info=True)
|
|
@@ -482,6 +504,7 @@ def index():
|
|
| 482 |
return redirect(url_for('admin_panel'))
|
| 483 |
|
| 484 |
# 1. Маршрут "Закуп"
|
|
|
|
| 485 |
@app.route('/procurement', methods=['GET', 'POST'])
|
| 486 |
def procurement():
|
| 487 |
data = load_data()
|
|
@@ -552,8 +575,11 @@ def procurement():
|
|
| 552 |
# Определение категории
|
| 553 |
final_category = new_category if new_category else (category if category and category != "__new__" else "Без категории")
|
| 554 |
# Добавляем новую категорию в общий список, если её там нет
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
# Поиск существующего материала (по названию, типу и категории)
|
| 559 |
existing_material_index = -1
|
|
@@ -607,8 +633,8 @@ def procurement():
|
|
| 607 |
data['materials'].extend(materials_to_add) # Добавляем новые
|
| 608 |
# data['materials'] уже содержит обновленные элементы, если были только обновления
|
| 609 |
|
| 610 |
-
# Обновляем и сортируем список категорий
|
| 611 |
-
data['categories'] = sorted(list(set(categories)), key=str.lower)
|
| 612 |
save_data(data)
|
| 613 |
flash(f"Закуп успешно зарегистрирован! Обработано {valid_items_processed} позиций.", "success")
|
| 614 |
upload_db_to_hf(DATA_FILE) # Запускаем бэкап
|
|
@@ -637,11 +663,13 @@ def procurement():
|
|
| 637 |
m_data['quantity_str'] = format_integer_py(m_data.get('quantity', '0'))
|
| 638 |
m_data['price_str'] = format_currency_py(m_data.get('price_per_unit', '0.00'))
|
| 639 |
materials_display.append(m_data)
|
| 640 |
-
|
|
|
|
| 641 |
html = BASE_TEMPLATE.replace('__TITLE__', "Закуп материалов").replace('__CONTENT__', PROCUREMENT_CONTENT).replace('__SCRIPTS__', PROCUREMENT_SCRIPTS)
|
| 642 |
-
return render_template_string(html, categories=
|
| 643 |
|
| 644 |
# 2. Маршрут "Раскрой"
|
|
|
|
| 645 |
@app.route('/cutting', methods=['GET', 'POST'])
|
| 646 |
def cutting():
|
| 647 |
data = load_data()
|
|
@@ -766,6 +794,7 @@ def cutting():
|
|
| 766 |
return render_template_string(html, fabrics=fabrics_display)
|
| 767 |
|
| 768 |
# 3. Маршрут "Пошив"
|
|
|
|
| 769 |
@app.route('/sewing', methods=['GET', 'POST'])
|
| 770 |
def sewing():
|
| 771 |
data = load_data()
|
|
@@ -1084,6 +1113,7 @@ def sewing():
|
|
| 1084 |
return render_template_string(html, cutting_tasks=tasks_for_template, fittings=fittings_for_template, all_materials=all_materials_for_template)
|
| 1085 |
|
| 1086 |
# 4. Маршрут "ОТК и Упаковка"
|
|
|
|
| 1087 |
@app.route('/qc_packing', methods=['GET', 'POST'])
|
| 1088 |
def qc_packing():
|
| 1089 |
data = load_data()
|
|
@@ -1325,8 +1355,8 @@ def qc_packing():
|
|
| 1325 |
html = BASE_TEMPLATE_OPERATIONAL.replace('__TITLE__', "ОТК и Упаковка").replace('__CONTENT__', QC_PACKING_CONTENT).replace('__SCRIPTS__', QC_PACKING_SCRIPTS)
|
| 1326 |
return render_template_string(html, sewing_tasks=tasks_for_template)
|
| 1327 |
|
| 1328 |
-
|
| 1329 |
# 5. Маршрут "База клиентов"
|
|
|
|
| 1330 |
@app.route('/clients', methods=['GET', 'POST'])
|
| 1331 |
def clients_panel():
|
| 1332 |
if request.method == 'POST':
|
|
@@ -1379,13 +1409,14 @@ def clients_panel():
|
|
| 1379 |
# Добавляем подробное логирование перед рендерингом
|
| 1380 |
logging.debug(f"Data for clients_panel template: clients type={type(clients_data)}, length={len(clients_data)}")
|
| 1381 |
for i, client in enumerate(clients_data):
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
|
|
|
| 1389 |
|
| 1390 |
html = BASE_TEMPLATE.replace('__TITLE__', "База клиентов").replace('__CONTENT__', CLIENTS_CONTENT).replace('__SCRIPTS__', CLIENTS_SCRIPTS)
|
| 1391 |
# Передаем обработанные данные в шаблон
|
|
@@ -1397,8 +1428,8 @@ def clients_panel():
|
|
| 1397 |
# Можно перенаправить на главную или показать пустую страницу
|
| 1398 |
return redirect(url_for('admin_panel'))
|
| 1399 |
|
| 1400 |
-
|
| 1401 |
# 6. Маршрут "Админ-панель"
|
|
|
|
| 1402 |
@app.route('/admin')
|
| 1403 |
def admin_panel():
|
| 1404 |
data = load_data()
|
|
@@ -1472,6 +1503,7 @@ def admin_panel():
|
|
| 1472 |
)
|
| 1473 |
|
| 1474 |
# 7. Маршрут для выполнения отправки
|
|
|
|
| 1475 |
@app.route('/dispatch_item', methods=['POST'])
|
| 1476 |
def dispatch_item():
|
| 1477 |
item_id = request.form.get('item_id')
|
|
@@ -1592,6 +1624,7 @@ def dispatch_item():
|
|
| 1592 |
return redirect(url_for('admin_panel') + '#dispatch-content')
|
| 1593 |
|
| 1594 |
# --- Остальные маршруты админ-панели ---
|
|
|
|
| 1595 |
@app.route('/admin/config/update', methods=['POST'])
|
| 1596 |
def update_config():
|
| 1597 |
data = load_data()
|
|
@@ -1657,11 +1690,14 @@ def add_category():
|
|
| 1657 |
flash("Название категории не может быть пустым.", "warning")
|
| 1658 |
return redirect(url_for('admin_panel')) # Возврат на админку
|
| 1659 |
|
|
|
|
|
|
|
|
|
|
| 1660 |
# Проверка на существование (без учета регистра)
|
| 1661 |
-
if new_category_name.lower() not in [c.lower() for c in
|
| 1662 |
-
|
| 1663 |
# Обновляем список категорий в данных и сортируем
|
| 1664 |
-
data['categories'] = sorted(list(set(
|
| 1665 |
save_data(data)
|
| 1666 |
flash(f"Категория '{new_category_name}' успешно добавлена.", "success")
|
| 1667 |
upload_db_to_hf(DATA_FILE) # Бэкап
|
|
@@ -1820,7 +1856,7 @@ def download_hf():
|
|
| 1820 |
# Странная ситуация, возможно, нет файлов или другая проблема
|
| 1821 |
flash("Не удалось инициировать скачивание файлов.", "warning")
|
| 1822 |
|
| 1823 |
-
# Перезагрузка данных в память после скачивания
|
| 1824 |
try:
|
| 1825 |
logging.info("Перезагрузка данных в память после скачивания...")
|
| 1826 |
load_data()
|
|
@@ -1833,11 +1869,12 @@ def download_hf():
|
|
| 1833 |
|
| 1834 |
return redirect(url_for('admin_panel'))
|
| 1835 |
|
| 1836 |
-
|
| 1837 |
-
# 8. Маршрут "Отчеты"
|
| 1838 |
@app.route('/reports', methods=['GET'])
|
| 1839 |
def reports():
|
|
|
|
| 1840 |
data = load_data()
|
|
|
|
| 1841 |
now = get_current_time() # Текущее время в Бишкеке
|
| 1842 |
|
| 1843 |
# Получение параметров фильтрации из URL
|
|
@@ -1856,65 +1893,48 @@ def reports():
|
|
| 1856 |
if filter_type == 'custom' and start_date_str and end_date_str:
|
| 1857 |
sd = datetime.strptime(start_date_str, '%Y-%m-%d')
|
| 1858 |
ed = datetime.strptime(end_date_str, '%Y-%m-%d')
|
| 1859 |
-
# Устанавливаем время: начало дня для start_date, конец дня для end_date
|
| 1860 |
start_date_dt = BISHKEK_TZ.localize(sd.replace(hour=0, minute=0, second=0, microsecond=0))
|
| 1861 |
end_date_dt = BISHKEK_TZ.localize(ed.replace(hour=23, minute=59, second=59, microsecond=999999))
|
| 1862 |
-
|
| 1863 |
elif filter_type == 'day':
|
| 1864 |
-
# Если дата не указана, берем сегодняшний день
|
| 1865 |
day_to_use_str = date_str if date_str else now.strftime('%Y-%m-%d')
|
| 1866 |
d = datetime.strptime(day_to_use_str, '%Y-%m-%d')
|
| 1867 |
start_date_dt = BISHKEK_TZ.localize(d.replace(hour=0, minute=0, second=0, microsecond=0))
|
| 1868 |
end_date_dt = start_date_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 1869 |
-
|
| 1870 |
elif filter_type == 'week':
|
| 1871 |
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
| 1872 |
-
# Начало недели (понедельник)
|
| 1873 |
start_date_dt = today_start - timedelta(days=today_start.weekday())
|
| 1874 |
-
# Конец недели (воскресенье, конец дня)
|
| 1875 |
end_date_dt = start_date_dt + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
| 1876 |
-
|
| 1877 |
elif filter_type == 'year':
|
| 1878 |
-
# Если год не указан, берем текущий
|
| 1879 |
year_to_use_str = year_str if year_str else str(now.year)
|
| 1880 |
year_int = int(year_to_use_str)
|
| 1881 |
start_date_dt = BISHKEK_TZ.localize(datetime(year_int, 1, 1, 0, 0, 0))
|
| 1882 |
end_date_dt = BISHKEK_TZ.localize(datetime(year_int, 12, 31, 23, 59, 59, 999999))
|
| 1883 |
-
|
| 1884 |
else: # По умолчанию 'month'
|
| 1885 |
-
# Если месяц не указан, берем текущий
|
| 1886 |
month_to_use_str = month_str if month_str else now.strftime('%Y-%m')
|
| 1887 |
year, month = map(int, month_to_use_str.split('-'))
|
| 1888 |
-
# Начало месяца
|
| 1889 |
start_date_dt = BISHKEK_TZ.localize(datetime(year, month, 1, 0, 0, 0))
|
| 1890 |
-
# Конец месяца (хитрый способ найти последний день)
|
| 1891 |
next_month_start = (start_date_dt.replace(day=28) + timedelta(days=4)).replace(day=1)
|
| 1892 |
end_date_dt = (next_month_start - timedelta(microseconds=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 1893 |
|
| 1894 |
-
# Проверка корректности диапазона
|
| 1895 |
if not start_date_dt or not end_date_dt or start_date_dt > end_date_dt:
|
| 1896 |
raise ValueError("Некорректный временной диапазон.")
|
| 1897 |
|
| 1898 |
except (ValueError, TypeError) as e:
|
| 1899 |
flash(f"Ошибка в задании периода: {e}. Отображен отчет за текущий месяц.", "warning")
|
| 1900 |
-
# Возвращаемся к текущему месяцу по умолчанию
|
| 1901 |
filter_type = 'month'
|
| 1902 |
start_date_dt = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
| 1903 |
next_month_start = (start_date_dt.replace(day=28) + timedelta(days=4)).replace(day=1)
|
| 1904 |
end_date_dt = (next_month_start - timedelta(microseconds=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 1905 |
|
| 1906 |
# --- Фильтрация данных по дате ---
|
| 1907 |
-
|
| 1908 |
-
# Упакованные изделия (фильтруем по timestamp_packed)
|
| 1909 |
filtered_packed_items = []
|
| 1910 |
all_packed_items_raw = data.get('qc_packing_items', [])
|
| 1911 |
for item_raw in all_packed_items_raw:
|
| 1912 |
if isinstance(item_raw, dict) and 'id' in item_raw:
|
| 1913 |
-
item_data = find_item_by_id(item_raw['id'], 'qc_packing_items')
|
| 1914 |
if not item_data: continue
|
| 1915 |
packed_time = parse_iso_datetime(item_data.get('timestamp_packed'))
|
| 1916 |
if packed_time and start_date_dt <= packed_time <= end_date_dt:
|
| 1917 |
-
# Добавляем время отправки, если есть
|
| 1918 |
shipment_time = None
|
| 1919 |
shipment_details = item_data.get('shipment_details')
|
| 1920 |
if shipment_details and shipment_details.get('timestamp'):
|
|
@@ -1922,23 +1942,21 @@ def reports():
|
|
| 1922 |
item_data['shipment_time_dt'] = shipment_time
|
| 1923 |
filtered_packed_items.append(item_data)
|
| 1924 |
|
| 1925 |
-
# Брак (фильтруем по timestamp)
|
| 1926 |
all_defect_log_raw = data.get('defect_log', [])
|
| 1927 |
filtered_defects = []
|
| 1928 |
for defect_raw in all_defect_log_raw:
|
| 1929 |
if isinstance(defect_raw, dict) and 'log_id' in defect_raw:
|
| 1930 |
-
defect_data = find_item_by_id(defect_raw['log_id'], 'defect_log')
|
| 1931 |
if not defect_data: continue
|
| 1932 |
defect_time = parse_iso_datetime(defect_data.get('timestamp'))
|
| 1933 |
if defect_time and start_date_dt <= defect_time <= end_date_dt:
|
| 1934 |
filtered_defects.append(defect_data)
|
| 1935 |
|
| 1936 |
-
# Дополнительные расходы (фильтруем по timestamp)
|
| 1937 |
all_expenses_raw = data.get('expenses', [])
|
| 1938 |
filtered_expenses = []
|
| 1939 |
for expense_raw in all_expenses_raw:
|
| 1940 |
if isinstance(expense_raw, dict) and 'id' in expense_raw:
|
| 1941 |
-
expense_data = find_item_by_id(expense_raw['id'], 'expenses')
|
| 1942 |
if not expense_data: continue
|
| 1943 |
expense_time = parse_iso_datetime(expense_data.get('timestamp'))
|
| 1944 |
if expense_time and start_date_dt <= expense_time <= end_date_dt:
|
|
@@ -1948,14 +1966,62 @@ def reports():
|
|
| 1948 |
total_packed_quantity = sum(item.get('quantity', 0) for item in filtered_packed_items)
|
| 1949 |
total_revenue = sum(item.get('packed_final_price', Decimal('0')) for item in filtered_packed_items)
|
| 1950 |
total_material_cost_packed = sum(item.get('packed_material_cost', Decimal('0')) for item in filtered_packed_items)
|
|
|
|
| 1951 |
total_salary_cost_packed = sum(item.get('packed_salary_cost', Decimal('0')) for item in filtered_packed_items)
|
| 1952 |
-
total_cost_packed = total_material_cost_packed + total_salary_cost_packed
|
| 1953 |
-
|
| 1954 |
-
total_defect_cost = sum(defect.get('cost_dec', Decimal('0')) for defect in filtered_defects)
|
| 1955 |
-
total_expenses_cost = sum(expense.get('amount', Decimal('0')) for expense in filtered_expenses)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1956 |
|
| 1957 |
-
total_overall_cost = total_cost_packed + total_defect_cost + total_expenses_cost # Все затраты
|
| 1958 |
-
total_profit = total_revenue - total_overall_cost # Прибыль
|
| 1959 |
|
| 1960 |
# --- Сводка по продуктам ---
|
| 1961 |
production_summary = {}
|
|
@@ -1979,24 +2045,28 @@ def reports():
|
|
| 1979 |
'total_packed_qty': total_packed_quantity,
|
| 1980 |
'total_revenue': total_revenue,
|
| 1981 |
'total_material_cost': total_material_cost_packed,
|
| 1982 |
-
'total_salary_cost': total_salary_cost_packed,
|
| 1983 |
'total_cost_packed': total_cost_packed,
|
| 1984 |
'total_defect_cost': total_defect_cost,
|
| 1985 |
'total_expenses': total_expenses_cost,
|
| 1986 |
'total_overall_cost': total_overall_cost,
|
| 1987 |
'total_profit': total_profit,
|
| 1988 |
-
|
| 1989 |
-
'
|
| 1990 |
-
'
|
| 1991 |
-
'
|
| 1992 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1993 |
'start_date': start_date_dt.strftime('%Y-%m-%d'),
|
| 1994 |
'end_date': end_date_dt.strftime('%Y-%m-%d'),
|
| 1995 |
'filter_type': filter_type,
|
| 1996 |
'current_day': now.strftime('%Y-%m-%d'),
|
| 1997 |
'current_month': now.strftime('%Y-%m'),
|
| 1998 |
'current_year': now.year,
|
| 1999 |
-
'filter_values': request.args
|
| 2000 |
}
|
| 2001 |
|
| 2002 |
html = BASE_TEMPLATE.replace('__TITLE__', "Отчеты").replace('__CONTENT__', REPORTS_CONTENT).replace('__SCRIPTS__', REPORTS_SCRIPTS)
|
|
@@ -2006,6 +2076,7 @@ def reports():
|
|
| 2006 |
# --- HTML Шаблоны ---
|
| 2007 |
|
| 2008 |
# ОБЫЧНЫЙ Базовый шаблон (с навигацией)
|
|
|
|
| 2009 |
BASE_TEMPLATE = """
|
| 2010 |
<!DOCTYPE html>
|
| 2011 |
<html lang="ru">
|
|
@@ -2064,6 +2135,8 @@ BASE_TEMPLATE = """
|
|
| 2064 |
.modal-body .list-group-item { font-size: 0.9rem; }
|
| 2065 |
.form-select-sm { padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: .875rem; }
|
| 2066 |
.btn-sm { padding: 0.25rem 0.5rem; font-size: .875rem; }
|
|
|
|
|
|
|
| 2067 |
</style>
|
| 2068 |
</head>
|
| 2069 |
<body>
|
|
@@ -2179,6 +2252,7 @@ __SCRIPTS__
|
|
| 2179 |
"""
|
| 2180 |
|
| 2181 |
# ОПЕРАЦИОННЫЙ Базовый шаблон (БЕЗ навигации и БЕЗ кнопки "Назад")
|
|
|
|
| 2182 |
BASE_TEMPLATE_OPERATIONAL = """
|
| 2183 |
<!DOCTYPE html>
|
| 2184 |
<html lang="ru">
|
|
@@ -2286,6 +2360,7 @@ __SCRIPTS__
|
|
| 2286 |
"""
|
| 2287 |
|
| 2288 |
# Контент закупки
|
|
|
|
| 2289 |
PROCUREMENT_CONTENT = """
|
| 2290 |
<div class="card">
|
| 2291 |
<div class="card-header"><i class="fas fa-plus-circle"></i>Добавить закупленные материалы</div>
|
|
@@ -2346,6 +2421,7 @@ PROCUREMENT_CONTENT = """
|
|
| 2346 |
"""
|
| 2347 |
|
| 2348 |
# Скрипты закупки
|
|
|
|
| 2349 |
PROCUREMENT_SCRIPTS = """
|
| 2350 |
<script>
|
| 2351 |
function addRow() {
|
|
@@ -2367,6 +2443,7 @@ PROCUREMENT_SCRIPTS = """
|
|
| 2367 |
"""
|
| 2368 |
|
| 2369 |
# Контент раскроя
|
|
|
|
| 2370 |
CUTTING_CONTENT = """
|
| 2371 |
<div class="card">
|
| 2372 |
<div class="card-header"><i class="fas fa-cut"></i>Регистрация раскроя</div>
|
|
@@ -2403,6 +2480,7 @@ CUTTING_CONTENT = """
|
|
| 2403 |
"""
|
| 2404 |
|
| 2405 |
# Скрипты раскроя
|
|
|
|
| 2406 |
CUTTING_SCRIPTS = """
|
| 2407 |
<script>
|
| 2408 |
function updateAvailableQuantity() {
|
|
@@ -2425,6 +2503,7 @@ CUTTING_SCRIPTS = """
|
|
| 2425 |
"""
|
| 2426 |
|
| 2427 |
# Контент пошива
|
|
|
|
| 2428 |
SEWING_CONTENT = """
|
| 2429 |
<div class="card">
|
| 2430 |
<div class="card-header"><i class="fas fa-tshirt"></i>Регистрация пошива</div>
|
|
@@ -2539,6 +2618,7 @@ SEWING_CONTENT = """
|
|
| 2539 |
"""
|
| 2540 |
|
| 2541 |
# Скрипты пошива
|
|
|
|
| 2542 |
SEWING_SCRIPTS = """
|
| 2543 |
<script>
|
| 2544 |
function showTaskDetails() {
|
|
@@ -2618,6 +2698,7 @@ SEWING_SCRIPTS = """
|
|
| 2618 |
"""
|
| 2619 |
|
| 2620 |
# Контент ОТК
|
|
|
|
| 2621 |
QC_PACKING_CONTENT = """
|
| 2622 |
<div class="card">
|
| 2623 |
<div class="card-header"><i class="fas fa-box-open"></i>ОТК и Упаковка готовых изделий</div>
|
|
@@ -2666,6 +2747,7 @@ QC_PACKING_CONTENT = """
|
|
| 2666 |
"""
|
| 2667 |
|
| 2668 |
# Скрипты ОТК
|
|
|
|
| 2669 |
QC_PACKING_SCRIPTS = """
|
| 2670 |
<script>
|
| 2671 |
let maxAllowedQuantity = 0;
|
|
@@ -2714,6 +2796,7 @@ QC_PACKING_SCRIPTS = """
|
|
| 2714 |
"""
|
| 2715 |
|
| 2716 |
# Контент Базы Клиентов
|
|
|
|
| 2717 |
CLIENTS_CONTENT = """
|
| 2718 |
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
| 2719 |
<h1 class="h2"><i class="fas fa-users me-2"></i>База клиентов</h1>
|
|
@@ -2834,8 +2917,8 @@ CLIENTS_CONTENT = """
|
|
| 2834 |
{% endfor %} {# Конец цикла по клиентам для модальных окон #}
|
| 2835 |
"""
|
| 2836 |
|
| 2837 |
-
|
| 2838 |
# Скрипты Базы Клиентов
|
|
|
|
| 2839 |
CLIENTS_SCRIPTS = """
|
| 2840 |
<script>
|
| 2841 |
const searchInput = document.getElementById('client-search');
|
|
@@ -2870,8 +2953,8 @@ document.addEventListener('DOMContentLoaded', function() {});
|
|
| 2870 |
</script>
|
| 2871 |
"""
|
| 2872 |
|
| 2873 |
-
|
| 2874 |
# Контент Админ-панели
|
|
|
|
| 2875 |
ADMIN_CONTENT = """
|
| 2876 |
<h1><i class="fas fa-tachometer-alt me-2"></i>Админ-панель</h1>
|
| 2877 |
<p class="lead">Обзор состояния производства и настройки</p>
|
|
@@ -3178,6 +3261,7 @@ ADMIN_CONTENT = """
|
|
| 3178 |
"""
|
| 3179 |
|
| 3180 |
# Скрипты Админ-панели
|
|
|
|
| 3181 |
ADMIN_SCRIPTS = """
|
| 3182 |
<script>
|
| 3183 |
// --- Поиск по таблице материалов ---
|
|
@@ -3240,7 +3324,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 3240 |
</script>
|
| 3241 |
"""
|
| 3242 |
|
| 3243 |
-
# Контент Отчетов
|
| 3244 |
REPORTS_CONTENT = """
|
| 3245 |
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
| 3246 |
<h1 class="h2"><i class="fas fa-chart-line me-2"></i>Отчеты</h1>
|
|
@@ -3330,15 +3414,17 @@ REPORTS_CONTENT = """
|
|
| 3330 |
</div>
|
| 3331 |
</div>
|
| 3332 |
<div class="col">
|
| 3333 |
-
|
|
|
|
| 3334 |
<div class="card-body">
|
| 3335 |
-
<h5 class="card-title"><i class="fas fa-
|
| 3336 |
<p class="card-text fs-5 mb-1">Материалы: {{ format_currency_py(report.total_material_cost) }} сом</p>
|
| 3337 |
<p class="card-text fs-5 mb-1">Зарплаты: {{ format_currency_py(report.total_salary_cost) }} сом</p>
|
| 3338 |
-
<p class="card-text fs-4 fw-bold mt-2"
|
| 3339 |
-
<small>За {{ format_integer_py(report.total_packed_qty) }}
|
| 3340 |
</div>
|
| 3341 |
</div>
|
|
|
|
| 3342 |
</div>
|
| 3343 |
<div class="col">
|
| 3344 |
<div class="card h-100 text-dark bg-info">
|
|
@@ -3362,7 +3448,9 @@ REPORTS_CONTENT = """
|
|
| 3362 |
<div class="tab-content" id="reportDetailsTabsContent">
|
| 3363 |
<!-- Сводка по продуктам -->
|
| 3364 |
<div class="tab-pane fade show active" id="prod-summary-content" role="tabpanel">
|
| 3365 |
-
|
|
|
|
|
|
|
| 3366 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3367 |
<th>Продукт</th><th>Упаковано (шт)</th><th>Выручка (сом)</th><th>Себестоимость (сом)</th><th>Прибыль (сом)</th><th>Средняя прибыль/шт</th>
|
| 3368 |
</tr></thead><tbody>
|
|
@@ -3380,7 +3468,9 @@ REPORTS_CONTENT = """
|
|
| 3380 |
</div>
|
| 3381 |
<!-- Упакованные изделия -->
|
| 3382 |
<div class="tab-pane fade" id="packed-items-content" role="tabpanel">
|
| 3383 |
-
|
|
|
|
|
|
|
| 3384 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3385 |
<th>ID</th><th>Название</th><th>Кол-во</th><th>Себест. (ед.)</th><th>Цена (ед.)</th><th>Общ. себест.</th><th>Общ. цена</th><th>Дата упак.</th><th>Статус</th><th>Д��тали отправки</th><th>Пошив ID</th>
|
| 3386 |
</tr></thead><tbody>
|
|
@@ -3417,7 +3507,9 @@ REPORTS_CONTENT = """
|
|
| 3417 |
</div>
|
| 3418 |
<!-- Брак -->
|
| 3419 |
<div class="tab-pane fade" id="defects-report-content" role="tabpanel">
|
| 3420 |
-
|
|
|
|
|
|
|
| 3421 |
<div class="table-responsive"><table class="table table-sm table-hover">
|
| 3422 |
<thead class="table-light"><tr>
|
| 3423 |
<th>ID</th><th>Материал/Изделие</th><th>Тип</th><th>Кол-во</th><th>Ед.</th><th>Стоимость</th><th>Этап</th><th>Причина</th><th>Дата</th><th>Пошив ID</th>
|
|
@@ -3438,7 +3530,9 @@ REPORTS_CONTENT = """
|
|
| 3438 |
</div>
|
| 3439 |
<!-- Доп. Расходы -->
|
| 3440 |
<div class="tab-pane fade" id="expenses-report-content" role="tabpanel">
|
| 3441 |
-
|
|
|
|
|
|
|
| 3442 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3443 |
<th>ID</th><th>Описание</th><th>Сумма (сом)</th><th>Дата</th>
|
| 3444 |
</tr></thead><tbody>
|
|
@@ -3451,9 +3545,47 @@ REPORTS_CONTENT = """
|
|
| 3451 |
</tbody></table></div></div></div>
|
| 3452 |
</div>
|
| 3453 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3454 |
"""
|
| 3455 |
|
| 3456 |
# Скрипты Отчетов
|
|
|
|
| 3457 |
REPORTS_SCRIPTS = """
|
| 3458 |
<script>
|
| 3459 |
document.addEventListener('DOMContentLoaded', function () {
|
|
@@ -3578,4 +3710,3 @@ if __name__ == '__main__':
|
|
| 3578 |
logging.info("Запуск Flask приложения на http://0.0.0.0:7860")
|
| 3579 |
# use_reloader=False важно при использовании бэкап-потока, чтобы он не перезапускался дважды
|
| 3580 |
app.run(debug=True, host='0.0.0.0', port=7860, use_reloader=False)
|
| 3581 |
-
|
|
|
|
| 86 |
if key not in data:
|
| 87 |
logging.warning(f"В {DATA_FILE} отсутствует ключ '{key}'. Инициализация значением по умолчанию.")
|
| 88 |
data[key] = default_data[key]
|
| 89 |
+
# Проверка типов данных верхнего уровня (добавлено)
|
| 90 |
+
elif not isinstance(data[key], type(default_data[key])):
|
| 91 |
+
logging.warning(f"В {DATA_FILE} ключ '{key}' имеет неверный тип ({type(data[key])} вместо {type(default_data[key])}). Инициализация значением по умолчанию.")
|
| 92 |
+
data[key] = default_data[key]
|
| 93 |
+
|
| 94 |
# Дополнительно проверяем config
|
| 95 |
if 'config' not in data or not isinstance(data['config'], dict):
|
| 96 |
logging.warning(f"В {DATA_FILE} отсутствует или некорректен ключ 'config'. Инициализация значением по умолчанию.")
|
|
|
|
| 101 |
if config_key not in data['config']:
|
| 102 |
logging.warning(f"В {DATA_FILE}['config'] отсутствует ключ '{config_key}'. Инициализация значением по умолчанию.")
|
| 103 |
data['config'][config_key] = default_value
|
| 104 |
+
# Проверка типа значения в config (добавлено)
|
| 105 |
+
elif not isinstance(data['config'][config_key], str): # В конфиге храним строки
|
| 106 |
+
logging.warning(f"В {DATA_FILE}['config'] ключ '{config_key}' имеет неверный тип ({type(data['config'][config_key])} вместо str). Попытка преобразования в строку.")
|
| 107 |
+
try:
|
| 108 |
+
data['config'][config_key] = str(data['config'][config_key])
|
| 109 |
+
except Exception:
|
| 110 |
+
logging.error(f"Не удалось преобразовать значение config '{config_key}' в строку. Установка значения по умолчанию.")
|
| 111 |
+
data['config'][config_key] = default_value
|
| 112 |
return data
|
| 113 |
except FileNotFoundError:
|
| 114 |
logging.warning(f"Локальный файл {DATA_FILE} не найден. Инициализация пустой структурой.")
|
| 115 |
return initialize_data_structure()
|
| 116 |
except json.JSONDecodeError:
|
| 117 |
logging.error(f"Ошибка декодирования JSON в файле {DATA_FILE}. Инициализация пустой структурой.")
|
|
|
|
| 118 |
try:
|
| 119 |
bad_file_path = f"{DATA_FILE}.{get_current_time().strftime('%Y%m%d%H%M%S')}.bad"
|
| 120 |
os.rename(DATA_FILE, bad_file_path)
|
|
|
|
| 123 |
logging.error(f"Не удалось создать бэкап поврежденного файла {DATA_FILE}: {backup_err}")
|
| 124 |
return initialize_data_structure()
|
| 125 |
except Exception as e:
|
| 126 |
+
logging.error(f"Неизвестная ошибка при загрузке локальных основных данных: {e}", exc_info=True)
|
| 127 |
return initialize_data_structure()
|
| 128 |
|
| 129 |
def save_data(data):
|
|
|
|
| 136 |
os.replace(temp_file, DATA_FILE)
|
| 137 |
logging.info(f"Основные данные успешно сохранены в локальный файл {DATA_FILE}.")
|
| 138 |
except Exception as e:
|
| 139 |
+
logging.error(f"Критическая ошибка при сохранении основных данных: {e}", exc_info=True)
|
| 140 |
if os.path.exists(temp_file):
|
| 141 |
try: os.remove(temp_file)
|
| 142 |
except OSError as rm_err: logging.error(f"Не удалось удалить временный файл {temp_file}: {rm_err}")
|
|
|
|
| 192 |
except FileNotFoundError: logging.warning(f"Локальный файл {CLIENT_DATA_FILE} не найден. Инициализация пустым списком."); return []
|
| 193 |
except json.JSONDecodeError:
|
| 194 |
logging.error(f"Ошибка декодирования JSON в файле {CLIENT_DATA_FILE}. Инициализация пустым списком.")
|
|
|
|
| 195 |
try:
|
| 196 |
bad_file_path = f"{CLIENT_DATA_FILE}.{get_current_time().strftime('%Y%m%d%H%M%S')}.bad"
|
| 197 |
os.rename(CLIENT_DATA_FILE, bad_file_path)
|
|
|
|
| 199 |
except Exception as backup_err:
|
| 200 |
logging.error(f"Не удалось создать бэкап поврежденного файла {CLIENT_DATA_FILE}: {backup_err}")
|
| 201 |
return []
|
| 202 |
+
except Exception as e: logging.error(f"Неизвестная ошибка при загрузке локальных данных клиентов: {e}", exc_info=True); return []
|
| 203 |
|
| 204 |
def save_client_data(clients):
|
| 205 |
"""Сохраняет данные клиентов в JSON файл."""
|
|
|
|
| 232 |
os.replace(temp_file, CLIENT_DATA_FILE)
|
| 233 |
logging.info(f"Данные клиентов успешно сохранены в локальный файл {CLIENT_DATA_FILE}.")
|
| 234 |
except Exception as e:
|
| 235 |
+
logging.error(f"Критическая ошибка при сохранении данных клиентов: {e}", exc_info=True)
|
| 236 |
if os.path.exists(temp_file):
|
| 237 |
try: os.remove(temp_file)
|
| 238 |
except OSError as rm_err: logging.error(f"Не удалось удалить временный файл {temp_file}: {rm_err}")
|
|
|
|
| 410 |
|
| 411 |
# Применяем преобразования
|
| 412 |
for field in decimal_fields:
|
| 413 |
+
# Проверяем, что значение существует перед преобразованием
|
| 414 |
+
if item_copy.get(field) is not None:
|
| 415 |
+
item_copy[field] = to_decimal(item_copy.get(field))
|
| 416 |
+
else:
|
| 417 |
+
# Устанавливаем значение по умолчанию, если поле отсутствует
|
| 418 |
+
item_copy[field] = Decimal('0.00') if field != 'quantity' else Decimal('0') # Пример
|
| 419 |
+
logging.warning(f"Поле '{field}' отсутствует в {item_list_name} ID {item_id}. Установлено значение по умолчанию.")
|
| 420 |
+
|
| 421 |
for field in int_fields:
|
| 422 |
+
if item_copy.get(field) is not None:
|
| 423 |
+
item_copy[field] = int(to_decimal(item_copy.get(field, '0')))
|
| 424 |
+
else:
|
| 425 |
+
item_copy[field] = 0
|
| 426 |
+
logging.warning(f"Поле '{field}' отсутствует в {item_list_name} ID {item_id}. Установлено значение 0.")
|
| 427 |
+
|
| 428 |
|
| 429 |
except Exception as conversion_error:
|
| 430 |
logging.error(f"Ошибка преобразования типов для {item_list_name} ID {item_id}: {conversion_error}", exc_info=True)
|
|
|
|
| 504 |
return redirect(url_for('admin_panel'))
|
| 505 |
|
| 506 |
# 1. Маршрут "Закуп"
|
| 507 |
+
# ... (Код маршрута /procurement без изменений) ...
|
| 508 |
@app.route('/procurement', methods=['GET', 'POST'])
|
| 509 |
def procurement():
|
| 510 |
data = load_data()
|
|
|
|
| 575 |
# Определение категории
|
| 576 |
final_category = new_category if new_category else (category if category and category != "__new__" else "Без категории")
|
| 577 |
# Добавляем новую категорию в общий список, если её там нет
|
| 578 |
+
# Убедимся, что работаем со списком строк
|
| 579 |
+
current_valid_categories = [c for c in categories if isinstance(c, str)]
|
| 580 |
+
if new_category and final_category not in current_valid_categories:
|
| 581 |
+
current_valid_categories.append(final_category)
|
| 582 |
+
categories = current_valid_categories # Обновляем основной список
|
| 583 |
|
| 584 |
# Поиск существующего материала (по названию, типу и категории)
|
| 585 |
existing_material_index = -1
|
|
|
|
| 633 |
data['materials'].extend(materials_to_add) # Добавляем новые
|
| 634 |
# data['materials'] уже содержит обновленные элементы, если были только обновления
|
| 635 |
|
| 636 |
+
# Обновляем и сортируем список категорий (только строки)
|
| 637 |
+
data['categories'] = sorted(list(set(c for c in categories if isinstance(c, str))), key=str.lower)
|
| 638 |
save_data(data)
|
| 639 |
flash(f"Закуп успешно зарегистрирован! Обработано {valid_items_processed} позиций.", "success")
|
| 640 |
upload_db_to_hf(DATA_FILE) # Запускаем бэкап
|
|
|
|
| 663 |
m_data['quantity_str'] = format_integer_py(m_data.get('quantity', '0'))
|
| 664 |
m_data['price_str'] = format_currency_py(m_data.get('price_per_unit', '0.00'))
|
| 665 |
materials_display.append(m_data)
|
| 666 |
+
# Фильтруем категории, оставляем только строки
|
| 667 |
+
valid_categories = [c for c in categories if isinstance(c, str)]
|
| 668 |
html = BASE_TEMPLATE.replace('__TITLE__', "Закуп материалов").replace('__CONTENT__', PROCUREMENT_CONTENT).replace('__SCRIPTS__', PROCUREMENT_SCRIPTS)
|
| 669 |
+
return render_template_string(html, categories=valid_categories, materials_display=materials_display)
|
| 670 |
|
| 671 |
# 2. Маршрут "Раскрой"
|
| 672 |
+
# ... (Код маршрута /cutting без изменений) ...
|
| 673 |
@app.route('/cutting', methods=['GET', 'POST'])
|
| 674 |
def cutting():
|
| 675 |
data = load_data()
|
|
|
|
| 794 |
return render_template_string(html, fabrics=fabrics_display)
|
| 795 |
|
| 796 |
# 3. Маршрут "Пошив"
|
| 797 |
+
# ... (Код маршрута /sewing без изменений) ...
|
| 798 |
@app.route('/sewing', methods=['GET', 'POST'])
|
| 799 |
def sewing():
|
| 800 |
data = load_data()
|
|
|
|
| 1113 |
return render_template_string(html, cutting_tasks=tasks_for_template, fittings=fittings_for_template, all_materials=all_materials_for_template)
|
| 1114 |
|
| 1115 |
# 4. Маршрут "ОТК и Упаковка"
|
| 1116 |
+
# ... (Код маршрута /qc_packing без изменений) ...
|
| 1117 |
@app.route('/qc_packing', methods=['GET', 'POST'])
|
| 1118 |
def qc_packing():
|
| 1119 |
data = load_data()
|
|
|
|
| 1355 |
html = BASE_TEMPLATE_OPERATIONAL.replace('__TITLE__', "ОТК и Упаковка").replace('__CONTENT__', QC_PACKING_CONTENT).replace('__SCRIPTS__', QC_PACKING_SCRIPTS)
|
| 1356 |
return render_template_string(html, sewing_tasks=tasks_for_template)
|
| 1357 |
|
|
|
|
| 1358 |
# 5. Маршрут "База клиентов"
|
| 1359 |
+
# ... (Код маршрута /clients без изменений) ...
|
| 1360 |
@app.route('/clients', methods=['GET', 'POST'])
|
| 1361 |
def clients_panel():
|
| 1362 |
if request.method == 'POST':
|
|
|
|
| 1409 |
# Добавляем подробное логирование перед рендерингом
|
| 1410 |
logging.debug(f"Data for clients_panel template: clients type={type(clients_data)}, length={len(clients_data)}")
|
| 1411 |
for i, client in enumerate(clients_data):
|
| 1412 |
+
hist = client.get('history')
|
| 1413 |
+
items_in_hist_status = []
|
| 1414 |
+
if isinstance(hist, list):
|
| 1415 |
+
for j, record in enumerate(hist):
|
| 1416 |
+
items = record.get('items')
|
| 1417 |
+
items_in_hist_status.append(f"rec{j}_items_type={type(items)}")
|
| 1418 |
+
logging.debug(f"Client {i} ({client.get('id')}): history type={type(hist)}, details=[{', '.join(items_in_hist_status)}]")
|
| 1419 |
+
|
| 1420 |
|
| 1421 |
html = BASE_TEMPLATE.replace('__TITLE__', "База клиентов").replace('__CONTENT__', CLIENTS_CONTENT).replace('__SCRIPTS__', CLIENTS_SCRIPTS)
|
| 1422 |
# Передаем обработанные данные в шаблон
|
|
|
|
| 1428 |
# Можно перенаправить на главную или показать пустую страницу
|
| 1429 |
return redirect(url_for('admin_panel'))
|
| 1430 |
|
|
|
|
| 1431 |
# 6. Маршрут "Админ-панель"
|
| 1432 |
+
# ... (Код маршрута /admin без изменений) ...
|
| 1433 |
@app.route('/admin')
|
| 1434 |
def admin_panel():
|
| 1435 |
data = load_data()
|
|
|
|
| 1503 |
)
|
| 1504 |
|
| 1505 |
# 7. Маршрут для выполнения отправки
|
| 1506 |
+
# ... (Код маршрута /dispatch_item без изменений) ...
|
| 1507 |
@app.route('/dispatch_item', methods=['POST'])
|
| 1508 |
def dispatch_item():
|
| 1509 |
item_id = request.form.get('item_id')
|
|
|
|
| 1624 |
return redirect(url_for('admin_panel') + '#dispatch-content')
|
| 1625 |
|
| 1626 |
# --- Остальные маршруты админ-панели ---
|
| 1627 |
+
# ... (Коды маршрутов /admin/config/update, /admin/expense/add, /admin/category/add, /admin/category/delete, /backup, /download без изменений) ...
|
| 1628 |
@app.route('/admin/config/update', methods=['POST'])
|
| 1629 |
def update_config():
|
| 1630 |
data = load_data()
|
|
|
|
| 1690 |
flash("Название категории не может быть пустым.", "warning")
|
| 1691 |
return redirect(url_for('admin_panel')) # Возврат на админку
|
| 1692 |
|
| 1693 |
+
# Фильтруем существующие категории, оставляем только строки
|
| 1694 |
+
current_valid_categories = [c for c in categories if isinstance(c, str)]
|
| 1695 |
+
|
| 1696 |
# Проверка на существование (без учета регистра)
|
| 1697 |
+
if new_category_name.lower() not in [c.lower() for c in current_valid_categories]:
|
| 1698 |
+
current_valid_categories.append(new_category_name)
|
| 1699 |
# Обновляем список категорий в данных и сортируем
|
| 1700 |
+
data['categories'] = sorted(list(set(current_valid_categories)), key=str.lower)
|
| 1701 |
save_data(data)
|
| 1702 |
flash(f"Категория '{new_category_name}' успешно добавлена.", "success")
|
| 1703 |
upload_db_to_hf(DATA_FILE) # Бэкап
|
|
|
|
| 1856 |
# Странная ситуация, возможно, нет файлов или другая проблема
|
| 1857 |
flash("Не удалось инициировать скачивание файлов.", "warning")
|
| 1858 |
|
| 1859 |
+
# Перезагрузка данных в память после скачивания
|
| 1860 |
try:
|
| 1861 |
logging.info("Перезагрузка данных в память после скачивания...")
|
| 1862 |
load_data()
|
|
|
|
| 1869 |
|
| 1870 |
return redirect(url_for('admin_panel'))
|
| 1871 |
|
| 1872 |
+
# 8. Маршрут "Отчеты" - ИЗМЕНЕНО: добавлен расчет ЗП по стадиям
|
|
|
|
| 1873 |
@app.route('/reports', methods=['GET'])
|
| 1874 |
def reports():
|
| 1875 |
+
# Загружаем основные данные и конфиг
|
| 1876 |
data = load_data()
|
| 1877 |
+
config = data.get('config', {})
|
| 1878 |
now = get_current_time() # Текущее время в Бишкеке
|
| 1879 |
|
| 1880 |
# Получение параметров фильтрации из URL
|
|
|
|
| 1893 |
if filter_type == 'custom' and start_date_str and end_date_str:
|
| 1894 |
sd = datetime.strptime(start_date_str, '%Y-%m-%d')
|
| 1895 |
ed = datetime.strptime(end_date_str, '%Y-%m-%d')
|
|
|
|
| 1896 |
start_date_dt = BISHKEK_TZ.localize(sd.replace(hour=0, minute=0, second=0, microsecond=0))
|
| 1897 |
end_date_dt = BISHKEK_TZ.localize(ed.replace(hour=23, minute=59, second=59, microsecond=999999))
|
|
|
|
| 1898 |
elif filter_type == 'day':
|
|
|
|
| 1899 |
day_to_use_str = date_str if date_str else now.strftime('%Y-%m-%d')
|
| 1900 |
d = datetime.strptime(day_to_use_str, '%Y-%m-%d')
|
| 1901 |
start_date_dt = BISHKEK_TZ.localize(d.replace(hour=0, minute=0, second=0, microsecond=0))
|
| 1902 |
end_date_dt = start_date_dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
|
|
|
| 1903 |
elif filter_type == 'week':
|
| 1904 |
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
|
|
| 1905 |
start_date_dt = today_start - timedelta(days=today_start.weekday())
|
|
|
|
| 1906 |
end_date_dt = start_date_dt + timedelta(days=6, hours=23, minutes=59, seconds=59, microseconds=999999)
|
|
|
|
| 1907 |
elif filter_type == 'year':
|
|
|
|
| 1908 |
year_to_use_str = year_str if year_str else str(now.year)
|
| 1909 |
year_int = int(year_to_use_str)
|
| 1910 |
start_date_dt = BISHKEK_TZ.localize(datetime(year_int, 1, 1, 0, 0, 0))
|
| 1911 |
end_date_dt = BISHKEK_TZ.localize(datetime(year_int, 12, 31, 23, 59, 59, 999999))
|
|
|
|
| 1912 |
else: # По умолчанию 'month'
|
|
|
|
| 1913 |
month_to_use_str = month_str if month_str else now.strftime('%Y-%m')
|
| 1914 |
year, month = map(int, month_to_use_str.split('-'))
|
|
|
|
| 1915 |
start_date_dt = BISHKEK_TZ.localize(datetime(year, month, 1, 0, 0, 0))
|
|
|
|
| 1916 |
next_month_start = (start_date_dt.replace(day=28) + timedelta(days=4)).replace(day=1)
|
| 1917 |
end_date_dt = (next_month_start - timedelta(microseconds=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 1918 |
|
|
|
|
| 1919 |
if not start_date_dt or not end_date_dt or start_date_dt > end_date_dt:
|
| 1920 |
raise ValueError("Некорректный временной диапазон.")
|
| 1921 |
|
| 1922 |
except (ValueError, TypeError) as e:
|
| 1923 |
flash(f"Ошибка в задании периода: {e}. Отображен отчет за текущий месяц.", "warning")
|
|
|
|
| 1924 |
filter_type = 'month'
|
| 1925 |
start_date_dt = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
| 1926 |
next_month_start = (start_date_dt.replace(day=28) + timedelta(days=4)).replace(day=1)
|
| 1927 |
end_date_dt = (next_month_start - timedelta(microseconds=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
| 1928 |
|
| 1929 |
# --- Фильтрация данных по дате ---
|
|
|
|
|
|
|
| 1930 |
filtered_packed_items = []
|
| 1931 |
all_packed_items_raw = data.get('qc_packing_items', [])
|
| 1932 |
for item_raw in all_packed_items_raw:
|
| 1933 |
if isinstance(item_raw, dict) and 'id' in item_raw:
|
| 1934 |
+
item_data = find_item_by_id(item_raw['id'], 'qc_packing_items')
|
| 1935 |
if not item_data: continue
|
| 1936 |
packed_time = parse_iso_datetime(item_data.get('timestamp_packed'))
|
| 1937 |
if packed_time and start_date_dt <= packed_time <= end_date_dt:
|
|
|
|
| 1938 |
shipment_time = None
|
| 1939 |
shipment_details = item_data.get('shipment_details')
|
| 1940 |
if shipment_details and shipment_details.get('timestamp'):
|
|
|
|
| 1942 |
item_data['shipment_time_dt'] = shipment_time
|
| 1943 |
filtered_packed_items.append(item_data)
|
| 1944 |
|
|
|
|
| 1945 |
all_defect_log_raw = data.get('defect_log', [])
|
| 1946 |
filtered_defects = []
|
| 1947 |
for defect_raw in all_defect_log_raw:
|
| 1948 |
if isinstance(defect_raw, dict) and 'log_id' in defect_raw:
|
| 1949 |
+
defect_data = find_item_by_id(defect_raw['log_id'], 'defect_log')
|
| 1950 |
if not defect_data: continue
|
| 1951 |
defect_time = parse_iso_datetime(defect_data.get('timestamp'))
|
| 1952 |
if defect_time and start_date_dt <= defect_time <= end_date_dt:
|
| 1953 |
filtered_defects.append(defect_data)
|
| 1954 |
|
|
|
|
| 1955 |
all_expenses_raw = data.get('expenses', [])
|
| 1956 |
filtered_expenses = []
|
| 1957 |
for expense_raw in all_expenses_raw:
|
| 1958 |
if isinstance(expense_raw, dict) and 'id' in expense_raw:
|
| 1959 |
+
expense_data = find_item_by_id(expense_raw['id'], 'expenses')
|
| 1960 |
if not expense_data: continue
|
| 1961 |
expense_time = parse_iso_datetime(expense_data.get('timestamp'))
|
| 1962 |
if expense_time and start_date_dt <= expense_time <= end_date_dt:
|
|
|
|
| 1966 |
total_packed_quantity = sum(item.get('quantity', 0) for item in filtered_packed_items)
|
| 1967 |
total_revenue = sum(item.get('packed_final_price', Decimal('0')) for item in filtered_packed_items)
|
| 1968 |
total_material_cost_packed = sum(item.get('packed_material_cost', Decimal('0')) for item in filtered_packed_items)
|
| 1969 |
+
# Общая ЗП из упакованных товаров (уже включает все 3 этапа)
|
| 1970 |
total_salary_cost_packed = sum(item.get('packed_salary_cost', Decimal('0')) for item in filtered_packed_items)
|
| 1971 |
+
total_cost_packed = total_material_cost_packed + total_salary_cost_packed
|
| 1972 |
+
|
| 1973 |
+
total_defect_cost = sum(defect.get('cost_dec', Decimal('0')) for defect in filtered_defects)
|
| 1974 |
+
total_expenses_cost = sum(expense.get('amount', Decimal('0')) for expense in filtered_expenses)
|
| 1975 |
+
|
| 1976 |
+
total_overall_cost = total_cost_packed + total_defect_cost + total_expenses_cost
|
| 1977 |
+
total_profit = total_revenue - total_overall_cost
|
| 1978 |
+
|
| 1979 |
+
# --- Расчет детализации ЗП по этапам за период ---
|
| 1980 |
+
total_cutter_salary = Decimal('0')
|
| 1981 |
+
total_sewer_salary = Decimal('0')
|
| 1982 |
+
total_packer_salary = Decimal('0')
|
| 1983 |
+
# Получаем ставки из конфига
|
| 1984 |
+
cutter_rate = to_decimal(config.get('salary_cutter_per_unit', '0'))
|
| 1985 |
+
sewer_rate = to_decimal(config.get('salary_sewer_per_unit', '0'))
|
| 1986 |
+
packer_rate = to_decimal(config.get('salary_packer_per_unit', '0'))
|
| 1987 |
+
|
| 1988 |
+
# Суммируем ЗП на основе КОЛИЧЕСТВА УПАКОВАННЫХ товаров в периоде
|
| 1989 |
+
# Важно: Мы предполагаем, что ЗП начисляется за ФАКТИЧЕСКИ УПАКОВАННЫЕ единицы.
|
| 1990 |
+
# ЗП раскройщика и швеи начисляется пропорционально упакованным, а не раскроенным/сшитым.
|
| 1991 |
+
for item in filtered_packed_items:
|
| 1992 |
+
qty = item.get('quantity', 0)
|
| 1993 |
+
# Здесь важно понимать, как начисляется ЗП. Если она начисляется
|
| 1994 |
+
# только за успешно упакованные, то этот расчет верен.
|
| 1995 |
+
# Если ЗП раскройщика/швеи начисляется за раскроенные/сшитые,
|
| 1996 |
+
# даже если они ушли в брак, логика будет сложнее (нужно будет
|
| 1997 |
+
# искать связанные задачи раскроя/пошива и суммировать их costs).
|
| 1998 |
+
# Текущая логика qc_packing записывает ЗП всех этапов в packed_salary_cost,
|
| 1999 |
+
# поэтому будем считать компоненты так же, как в qc_packing.
|
| 2000 |
+
|
| 2001 |
+
# --- Версия 1: Расчет на основе ставок и кол-ва упакованных ---
|
| 2002 |
+
total_cutter_salary += Decimal(qty) * cutter_rate
|
| 2003 |
+
total_sewer_salary += Decimal(qty) * sewer_rate
|
| 2004 |
+
total_packer_salary += Decimal(qty) * packer_rate
|
| 2005 |
+
|
| 2006 |
+
# --- Версия 2 (если нужно брать из связанных задач, сложнее): ---
|
| 2007 |
+
# Нужно найти sewing_task, потом cutting_task и взять их salary_cost,
|
| 2008 |
+
# затем рассчитать долю для qty упакованных.
|
| 2009 |
+
# Примерно:
|
| 2010 |
+
# sewing_task = find_item_by_id(item.get('sewing_task_id'), 'sewing_tasks')
|
| 2011 |
+
# if sewing_task:
|
| 2012 |
+
# sewn_qty = sewing_task.get('sewn_quantity', 1) or 1
|
| 2013 |
+
# sewing_sal_total = sewing_task.get('sewing_salary_cost', Decimal('0'))
|
| 2014 |
+
# cutting_sal_total_from_sew = sewing_task.get('cutting_salary_cost', Decimal('0')) # Если передали при пошиве
|
| 2015 |
+
# total_sewer_salary += (sewing_sal_total / Decimal(sewn_qty)) * Decimal(qty)
|
| 2016 |
+
# total_cutter_salary += (cutting_sal_total_from_sew / Decimal(sewn_qty)) * Decimal(qty) # Примерная логика
|
| 2017 |
+
# total_packer_salary += Decimal(qty) * packer_rate
|
| 2018 |
+
|
| 2019 |
+
|
| 2020 |
+
# Проверка: сумма компонентов должна быть равна общей ЗП (может быть погрешность из-за округления)
|
| 2021 |
+
calculated_total_salary = total_cutter_salary + total_sewer_salary + total_packer_salary
|
| 2022 |
+
if abs(calculated_total_salary - total_salary_cost_packed) > Decimal('0.01') * total_packed_quantity: # Допуск 1 копейка на ед.
|
| 2023 |
+
logging.warning(f"Расчетная детализация ЗП ({calculated_total_salary}) не совпадает с общей ЗП из упаковок ({total_salary_cost_packed}). Возможны расхождения в логике или округлении.")
|
| 2024 |
|
|
|
|
|
|
|
| 2025 |
|
| 2026 |
# --- Сводка по продуктам ---
|
| 2027 |
production_summary = {}
|
|
|
|
| 2045 |
'total_packed_qty': total_packed_quantity,
|
| 2046 |
'total_revenue': total_revenue,
|
| 2047 |
'total_material_cost': total_material_cost_packed,
|
| 2048 |
+
'total_salary_cost': total_salary_cost_packed, # Общая ЗП
|
| 2049 |
'total_cost_packed': total_cost_packed,
|
| 2050 |
'total_defect_cost': total_defect_cost,
|
| 2051 |
'total_expenses': total_expenses_cost,
|
| 2052 |
'total_overall_cost': total_overall_cost,
|
| 2053 |
'total_profit': total_profit,
|
| 2054 |
+
# Добавлена детализация ЗП
|
| 2055 |
+
'total_cutter_salary': total_cutter_salary,
|
| 2056 |
+
'total_sewer_salary': total_sewer_salary,
|
| 2057 |
+
'total_packer_salary': total_packer_salary,
|
| 2058 |
+
# ---
|
| 2059 |
+
'production_summary': production_summary,
|
| 2060 |
+
'filtered_packed_items': filtered_packed_items,
|
| 2061 |
+
'filtered_defects': filtered_defects,
|
| 2062 |
+
'filtered_expenses': filtered_expenses,
|
| 2063 |
'start_date': start_date_dt.strftime('%Y-%m-%d'),
|
| 2064 |
'end_date': end_date_dt.strftime('%Y-%m-%d'),
|
| 2065 |
'filter_type': filter_type,
|
| 2066 |
'current_day': now.strftime('%Y-%m-%d'),
|
| 2067 |
'current_month': now.strftime('%Y-%m'),
|
| 2068 |
'current_year': now.year,
|
| 2069 |
+
'filter_values': request.args
|
| 2070 |
}
|
| 2071 |
|
| 2072 |
html = BASE_TEMPLATE.replace('__TITLE__', "Отчеты").replace('__CONTENT__', REPORTS_CONTENT).replace('__SCRIPTS__', REPORTS_SCRIPTS)
|
|
|
|
| 2076 |
# --- HTML Шаблоны ---
|
| 2077 |
|
| 2078 |
# ОБЫЧНЫЙ Базовый шаблон (с навигацией)
|
| 2079 |
+
# ... (BASE_TEMPLATE без изменений) ...
|
| 2080 |
BASE_TEMPLATE = """
|
| 2081 |
<!DOCTYPE html>
|
| 2082 |
<html lang="ru">
|
|
|
|
| 2135 |
.modal-body .list-group-item { font-size: 0.9rem; }
|
| 2136 |
.form-select-sm { padding-top: 0.25rem; padding-bottom: 0.25rem; font-size: .875rem; }
|
| 2137 |
.btn-sm { padding: 0.25rem 0.5rem; font-size: .875rem; }
|
| 2138 |
+
.clickable-card { cursor: pointer; } /* Для кликабельных карточек */
|
| 2139 |
+
.clickable-card:hover { box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.15); } /* Эффект при наведении */
|
| 2140 |
</style>
|
| 2141 |
</head>
|
| 2142 |
<body>
|
|
|
|
| 2252 |
"""
|
| 2253 |
|
| 2254 |
# ОПЕРАЦИОННЫЙ Базовый шаблон (БЕЗ навигации и БЕЗ кнопки "Назад")
|
| 2255 |
+
# ... (BASE_TEMPLATE_OPERATIONAL без изменений) ...
|
| 2256 |
BASE_TEMPLATE_OPERATIONAL = """
|
| 2257 |
<!DOCTYPE html>
|
| 2258 |
<html lang="ru">
|
|
|
|
| 2360 |
"""
|
| 2361 |
|
| 2362 |
# Контент закупки
|
| 2363 |
+
# ... (PROCUREMENT_CONTENT без изменений) ...
|
| 2364 |
PROCUREMENT_CONTENT = """
|
| 2365 |
<div class="card">
|
| 2366 |
<div class="card-header"><i class="fas fa-plus-circle"></i>Добавить закупленные материалы</div>
|
|
|
|
| 2421 |
"""
|
| 2422 |
|
| 2423 |
# Скрипты закупки
|
| 2424 |
+
# ... (PROCUREMENT_SCRIPTS без изменений) ...
|
| 2425 |
PROCUREMENT_SCRIPTS = """
|
| 2426 |
<script>
|
| 2427 |
function addRow() {
|
|
|
|
| 2443 |
"""
|
| 2444 |
|
| 2445 |
# Контент раскроя
|
| 2446 |
+
# ... (CUTTING_CONTENT без изменений) ...
|
| 2447 |
CUTTING_CONTENT = """
|
| 2448 |
<div class="card">
|
| 2449 |
<div class="card-header"><i class="fas fa-cut"></i>Регистрация раскроя</div>
|
|
|
|
| 2480 |
"""
|
| 2481 |
|
| 2482 |
# Скрипты раскроя
|
| 2483 |
+
# ... (CUTTING_SCRIPTS без изменений) ...
|
| 2484 |
CUTTING_SCRIPTS = """
|
| 2485 |
<script>
|
| 2486 |
function updateAvailableQuantity() {
|
|
|
|
| 2503 |
"""
|
| 2504 |
|
| 2505 |
# Контент пошива
|
| 2506 |
+
# ... (SEWING_CONTENT без изменений) ...
|
| 2507 |
SEWING_CONTENT = """
|
| 2508 |
<div class="card">
|
| 2509 |
<div class="card-header"><i class="fas fa-tshirt"></i>Регистрация пошива</div>
|
|
|
|
| 2618 |
"""
|
| 2619 |
|
| 2620 |
# Скрипты пошива
|
| 2621 |
+
# ... (SEWING_SCRIPTS без изменений) ...
|
| 2622 |
SEWING_SCRIPTS = """
|
| 2623 |
<script>
|
| 2624 |
function showTaskDetails() {
|
|
|
|
| 2698 |
"""
|
| 2699 |
|
| 2700 |
# Контент ОТК
|
| 2701 |
+
# ... (QC_PACKING_CONTENT без изменений) ...
|
| 2702 |
QC_PACKING_CONTENT = """
|
| 2703 |
<div class="card">
|
| 2704 |
<div class="card-header"><i class="fas fa-box-open"></i>ОТК и Упаковка готовых изделий</div>
|
|
|
|
| 2747 |
"""
|
| 2748 |
|
| 2749 |
# Скрипты ОТК
|
| 2750 |
+
# ... (QC_PACKING_SCRIPTS без изменений) ...
|
| 2751 |
QC_PACKING_SCRIPTS = """
|
| 2752 |
<script>
|
| 2753 |
let maxAllowedQuantity = 0;
|
|
|
|
| 2796 |
"""
|
| 2797 |
|
| 2798 |
# Контент Базы Клиентов
|
| 2799 |
+
# ... (CLIENTS_CONTENT без изменений) ...
|
| 2800 |
CLIENTS_CONTENT = """
|
| 2801 |
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
| 2802 |
<h1 class="h2"><i class="fas fa-users me-2"></i>База клиентов</h1>
|
|
|
|
| 2917 |
{% endfor %} {# Конец цикла по клиентам для модальных окон #}
|
| 2918 |
"""
|
| 2919 |
|
|
|
|
| 2920 |
# Скрипты Базы Клиентов
|
| 2921 |
+
# ... (CLIENTS_SCRIPTS без изменений) ...
|
| 2922 |
CLIENTS_SCRIPTS = """
|
| 2923 |
<script>
|
| 2924 |
const searchInput = document.getElementById('client-search');
|
|
|
|
| 2953 |
</script>
|
| 2954 |
"""
|
| 2955 |
|
|
|
|
| 2956 |
# Контент Админ-панели
|
| 2957 |
+
# ... (ADMIN_CONTENT без изменений) ...
|
| 2958 |
ADMIN_CONTENT = """
|
| 2959 |
<h1><i class="fas fa-tachometer-alt me-2"></i>Админ-панель</h1>
|
| 2960 |
<p class="lead">Обзор состояния производства и настройки</p>
|
|
|
|
| 3261 |
"""
|
| 3262 |
|
| 3263 |
# Скрипты Админ-панели
|
| 3264 |
+
# ... (ADMIN_SCRIPTS без изменений) ...
|
| 3265 |
ADMIN_SCRIPTS = """
|
| 3266 |
<script>
|
| 3267 |
// --- Поиск по таблице материалов ---
|
|
|
|
| 3324 |
</script>
|
| 3325 |
"""
|
| 3326 |
|
| 3327 |
+
# Контент Отчетов - ИЗМЕНЕНО: добавлена кликабельная карточка ЗП и модальное окно
|
| 3328 |
REPORTS_CONTENT = """
|
| 3329 |
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
| 3330 |
<h1 class="h2"><i class="fas fa-chart-line me-2"></i>Отчеты</h1>
|
|
|
|
| 3414 |
</div>
|
| 3415 |
</div>
|
| 3416 |
<div class="col">
|
| 3417 |
+
{# --- ИЗМЕНЕНО: Карточка ЗП стала кликабельной --- #}
|
| 3418 |
+
<div class="card h-100 text-dark bg-light clickable-card" data-bs-toggle="modal" data-bs-target="#salaryBreakdownModal" title="Нажмите для просмотра детализации ЗП">
|
| 3419 |
<div class="card-body">
|
| 3420 |
+
<h5 class="card-title"><i class="fas fa-users-cog me-2"></i>ЗП (Упакованные)</h5>
|
| 3421 |
<p class="card-text fs-5 mb-1">Материалы: {{ format_currency_py(report.total_material_cost) }} сом</p>
|
| 3422 |
<p class="card-text fs-5 mb-1">Зарплаты: {{ format_currency_py(report.total_salary_cost) }} сом</p>
|
| 3423 |
+
<p class="card-text fs-4 fw-bold mt-2">Итого себестоимость: {{ format_currency_py(report.total_cost_packed) }} сом</p>
|
| 3424 |
+
<small>За {{ format_integer_py(report.total_packed_qty) }} шт. <span class="text-primary">(Нажмите для деталей ЗП)</span></small>
|
| 3425 |
</div>
|
| 3426 |
</div>
|
| 3427 |
+
{# --- КОНЕЦ ИЗМЕНЕНИЯ --- #}
|
| 3428 |
</div>
|
| 3429 |
<div class="col">
|
| 3430 |
<div class="card h-100 text-dark bg-info">
|
|
|
|
| 3448 |
<div class="tab-content" id="reportDetailsTabsContent">
|
| 3449 |
<!-- Сводка по продуктам -->
|
| 3450 |
<div class="tab-pane fade show active" id="prod-summary-content" role="tabpanel">
|
| 3451 |
+
{# --- ИЗМЕНЕНО: Добавлен bg-white --- #}
|
| 3452 |
+
<div class="card"><div class="card-header"><i class="fas fa-tags"></i>Сводка по продуктам за период (по дате упаковки)</div><div class="card-body bg-white">
|
| 3453 |
+
{# --- КОНЕЦ ИЗМЕНЕНИЯ --- #}
|
| 3454 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3455 |
<th>Продукт</th><th>Упаковано (шт)</th><th>Выручка (сом)</th><th>Себестоимость (сом)</th><th>Прибыль (сом)</th><th>Средняя прибыль/шт</th>
|
| 3456 |
</tr></thead><tbody>
|
|
|
|
| 3468 |
</div>
|
| 3469 |
<!-- Упакованные изделия -->
|
| 3470 |
<div class="tab-pane fade" id="packed-items-content" role="tabpanel">
|
| 3471 |
+
{# --- ИЗМЕНЕНО: Добавлен bg-white --- #}
|
| 3472 |
+
<div class="card"><div class="card-header"><i class="fas fa-check-double"></i>Упакованные изделия за период</div><div class="card-body bg-white">
|
| 3473 |
+
{# --- КОНЕЦ ИЗМЕНЕНИЯ --- #}
|
| 3474 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3475 |
<th>ID</th><th>Название</th><th>Кол-во</th><th>Себест. (ед.)</th><th>Цена (ед.)</th><th>Общ. себест.</th><th>Общ. цена</th><th>Дата упак.</th><th>Статус</th><th>Д��тали отправки</th><th>Пошив ID</th>
|
| 3476 |
</tr></thead><tbody>
|
|
|
|
| 3507 |
</div>
|
| 3508 |
<!-- Брак -->
|
| 3509 |
<div class="tab-pane fade" id="defects-report-content" role="tabpanel">
|
| 3510 |
+
{# --- ИЗМЕНЕНО: Добавлен bg-white --- #}
|
| 3511 |
+
<div class="card"><div class="card-header"><i class="fas fa-exclamation-triangle"></i>Брак за период</div><div class="card-body bg-white">
|
| 3512 |
+
{# --- КОНЕЦ ИЗМЕНЕНИЯ --- #}
|
| 3513 |
<div class="table-responsive"><table class="table table-sm table-hover">
|
| 3514 |
<thead class="table-light"><tr>
|
| 3515 |
<th>ID</th><th>Материал/Изделие</th><th>Тип</th><th>Кол-во</th><th>Ед.</th><th>Стоимость</th><th>Этап</th><th>Причина</th><th>Дата</th><th>Пошив ID</th>
|
|
|
|
| 3530 |
</div>
|
| 3531 |
<!-- Доп. Расходы -->
|
| 3532 |
<div class="tab-pane fade" id="expenses-report-content" role="tabpanel">
|
| 3533 |
+
{# --- ИЗМЕНЕНО: Добавлен bg-white --- #}
|
| 3534 |
+
<div class="card"><div class="card-header"><i class="fas fa-file-invoice-dollar"></i>Доп. расходы за период</div><div class="card-body bg-white">
|
| 3535 |
+
{# --- КОНЕЦ ИЗМЕНЕНИЯ --- #}
|
| 3536 |
<div class="table-responsive"><table class="table table-sm table-hover"><thead><tr>
|
| 3537 |
<th>ID</th><th>Описание</th><th>Сумма (сом)</th><th>Дата</th>
|
| 3538 |
</tr></thead><tbody>
|
|
|
|
| 3545 |
</tbody></table></div></div></div>
|
| 3546 |
</div>
|
| 3547 |
</div>
|
| 3548 |
+
|
| 3549 |
+
{# --- НАЧАЛО: Модальное окно для детализации ЗП --- #}
|
| 3550 |
+
<div class="modal fade" id="salaryBreakdownModal" tabindex="-1" aria-labelledby="salaryBreakdownModalLabel" aria-hidden="true">
|
| 3551 |
+
<div class="modal-dialog">
|
| 3552 |
+
<div class="modal-content">
|
| 3553 |
+
<div class="modal-header">
|
| 3554 |
+
<h5 class="modal-title" id="salaryBreakdownModalLabel">Детализация зарплат за период ({{ report.start_date }} - {{ report.end_date }})</h5>
|
| 3555 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 3556 |
+
</div>
|
| 3557 |
+
<div class="modal-body">
|
| 3558 |
+
<ul class="list-group list-group-flush">
|
| 3559 |
+
<li class="list-group-item d-flex justify-content-between align-items-center">
|
| 3560 |
+
ЗП Раскройщиков:
|
| 3561 |
+
<span class="badge bg-info rounded-pill">{{ format_currency_py(report.total_cutter_salary) }} сом</span>
|
| 3562 |
+
</li>
|
| 3563 |
+
<li class="list-group-item d-flex justify-content-between align-items-center">
|
| 3564 |
+
ЗП Швей:
|
| 3565 |
+
<span class="badge bg-warning rounded-pill text-dark">{{ format_currency_py(report.total_sewer_salary) }} сом</span>
|
| 3566 |
+
</li>
|
| 3567 |
+
<li class="list-group-item d-flex justify-content-between align-items-center">
|
| 3568 |
+
ЗП Упаковщиков:
|
| 3569 |
+
<span class="badge bg-success rounded-pill">{{ format_currency_py(report.total_packer_salary) }} сом</span>
|
| 3570 |
+
</li>
|
| 3571 |
+
<li class="list-group-item d-flex justify-content-between align-items-center fw-bold">
|
| 3572 |
+
Итого ЗП (Упакованные):
|
| 3573 |
+
<span class="badge bg-primary rounded-pill">{{ format_currency_py(report.total_salary_cost) }} сом</span>
|
| 3574 |
+
</li>
|
| 3575 |
+
</ul>
|
| 3576 |
+
<small class="d-block text-muted mt-2">Примечание: Расчет ЗП по этапам основан на количестве фактически упакованных изделий в выбранный период и текущих ставках ЗП из настроек.</small>
|
| 3577 |
+
</div>
|
| 3578 |
+
<div class="modal-footer">
|
| 3579 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
| 3580 |
+
</div>
|
| 3581 |
+
</div>
|
| 3582 |
+
</div>
|
| 3583 |
+
</div>
|
| 3584 |
+
{# --- КОНЕЦ: Модальное окно для детализации ЗП --- #}
|
| 3585 |
"""
|
| 3586 |
|
| 3587 |
# Скрипты Отчетов
|
| 3588 |
+
# ... (REPORTS_SCRIPTS без изменений) ...
|
| 3589 |
REPORTS_SCRIPTS = """
|
| 3590 |
<script>
|
| 3591 |
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
| 3710 |
logging.info("Запуск Flask приложения на http://0.0.0.0:7860")
|
| 3711 |
# use_reloader=False важно при использовании бэкап-потока, чтобы он не перезапускался дважды
|
| 3712 |
app.run(debug=True, host='0.0.0.0', port=7860, use_reloader=False)
|
|
|