Kgshop commited on
Commit
78c0723
·
verified ·
1 Parent(s): 6e11ad2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -77
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
- item_copy[field] = to_decimal(item_copy.get(field))
 
 
 
 
 
 
 
403
  for field in int_fields:
404
- # Используем to_decimal перед int для обработки строк типа "10.0"
405
- item_copy[field] = int(to_decimal(item_copy.get(field, '0')))
 
 
 
 
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
- if new_category and final_category not in categories:
556
- categories.append(final_category)
 
 
 
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=categories, materials_display=materials_display)
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
- hist = client.get('history')
1383
- items_in_hist_status = []
1384
- if isinstance(hist, list):
1385
- for j, record in enumerate(hist):
1386
- items = record.get('items')
1387
- items_in_hist_status.append(f"rec{j}_items_type={type(items)}")
1388
- logging.debug(f"Client {i} ({client.get('id')}): history type={type(hist)}, details=[{', '.join(items_in_hist_status)}]")
 
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 categories if isinstance(c, str)]: # Добавили isinstance
1662
- categories.append(new_category_name)
1663
  # Обновляем список категорий в данных и сортируем
1664
- data['categories'] = sorted(list(set(c for c in categories if isinstance(c, str))), key=str.lower) # Фильтруем не-строки перед 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') # Получаем с Decimal
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') # Получаем с Decimal
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') # Получаем с Decimal
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
- 'production_summary': production_summary, # Словарь со сводкой
1989
- 'filtered_packed_items': filtered_packed_items, # Список упакованных
1990
- 'filtered_defects': filtered_defects, # Список брака
1991
- 'filtered_expenses': filtered_expenses, # Список доп. расходов
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 # Передаем все GET-параметры для удобства
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
- <div class="card h-100 text-dark bg-light">
 
3334
  <div class="card-body">
3335
- <h5 class="card-title"><i class="fas fa-box me-2"></i>Себест. упак.</h5>
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">Итого: {{ format_currency_py(report.total_cost_packed) }} сом</p>
3339
- <small>За {{ format_integer_py(report.total_packed_qty) }} шт.</small>
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
- <div class="card"><div class="card-header"><i class="fas fa-tags"></i>Сводка по продуктам за период (по дате упаковки)</div><div class="card-body">
 
 
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
- <div class="card"><div class="card-header"><i class="fas fa-check-double"></i>Упакованные изделия за период</div><div class="card-body">
 
 
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
- <div class="card"><div class="card-header"><i class="fas fa-exclamation-triangle"></i>Брак за период</div><div class="card-body">
 
 
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
- <div class="card"><div class="card-header"><i class="fas fa-file-invoice-dollar"></i>Доп. расходы за период</div><div class="card-body">
 
 
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)