Kgshop commited on
Commit
87591f4
·
verified ·
1 Parent(s): 0b6a423

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -126
app.py CHANGED
@@ -95,7 +95,7 @@ def load_json_data(file_key):
95
  with open(filepath, 'r', encoding='utf-8') as f:
96
  return json.load(f)
97
  except (FileNotFoundError, json.JSONDecodeError):
98
- return []
99
 
100
  def save_json_data(file_key, data):
101
  filepath, lock = DATA_FILES[file_key]
@@ -162,64 +162,73 @@ def format_currency_py(value):
162
  return "0"
163
 
164
  def generate_receipt_html(transaction):
165
- has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items', []))
166
  delivery_cost = to_decimal(transaction.get('delivery_cost', '0'))
167
  note = transaction.get('note', '')
168
 
169
  if has_discounts:
170
  table_headers = """
171
- <th style="text-align: center; width: 5%;">№</th>
172
- <th style="text-align: left;">Наименование</th>
173
- <th style="text-align: right;">Кол-во</th>
174
- <th style="text-align: right;">Цена</th>
175
- <th style="text-align: right;">Скидка</th>
176
- <th style="text-align: right;">Сумма</th>
177
  """
178
  total_colspan = 5
179
  else:
180
  table_headers = """
181
- <th style="text-align: center; width: 5%;">№</th>
182
- <th style="text-align: left;">Наименование</th>
183
- <th style="text-align: right;">Кол-во</th>
184
- <th style="text-align: right;">Цена</th>
185
- <th style="text-align: right;">Сумма</th>
186
  """
187
  total_colspan = 4
188
 
189
  items_html = ""
 
190
  for i, item in enumerate(transaction['items']):
191
- discount_cell = f"""<td style="text-align: right;">{format_currency_py(item.get('discount_per_item', '0'))}</td>""" if has_discounts else ""
 
192
  items_html += f"""
193
  <tr>
194
- <td style="text-align: center;">{i + 1}</td>
195
- <td>{item['name']}</td>
196
- <td style="text-align: right;">{item['quantity']}</td>
197
- <td style="text-align: right;">{format_currency_py(item['price_at_sale'])}</td>
198
  {discount_cell}
199
- <td style="text-align: right;">{format_currency_py(item['total'])}</td>
200
  </tr>
201
  """
202
 
203
  total_amount_from_db = to_decimal(transaction['total_amount'])
204
 
205
  totals_html = ""
 
 
 
 
 
 
 
206
  if delivery_cost > 0:
207
  subtotal = total_amount_from_db - delivery_cost
208
  totals_html += f"""
209
  <tr>
210
- <td colspan="{total_colspan}" style="text-align: right;">Подытог:</td>
211
- <td style="text-align: right;">{format_currency_py(subtotal)} ₸</td>
212
  </tr>
213
  <tr>
214
- <td colspan="{total_colspan}" style="text-align: right;">Доставка:</td>
215
- <td style="text-align: right;">{format_currency_py(delivery_cost)} ₸</td>
216
  </tr>
217
  """
218
 
219
  totals_html += f"""
220
  <tr class="total">
221
- <td colspan="{total_colspan}" style="text-align: right;">Итого к оплате:</td>
222
- <td style="text-align: right;">{format_currency_py(total_amount_from_db)} ₸</td>
223
  </tr>
224
  """
225
 
@@ -256,8 +265,8 @@ def generate_receipt_html(transaction):
256
  .header p {{ margin: 2px 0; font-size: 14px; }}
257
  .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
258
  table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
259
- table th {{ background: #f2f2f2; font-weight: bold; padding: 8px; border-bottom: 2px solid #ddd; }}
260
- table td {{ padding: 8px; border-bottom: 1px solid #eee; }}
261
  table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
262
  .footer-info {{ font-size: 14px; margin-top: 20px; }}
263
  .print-hide {{ display: block; }}
@@ -360,22 +369,16 @@ def sales_screen():
360
  transactions = load_json_data('transactions')
361
  edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
362
 
363
- active_inventory = []
364
  for p in inventory:
365
- if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants', [])):
366
  active_inventory.append(p)
367
 
368
  active_inventory.sort(key=lambda x: x.get('name', '').lower())
369
-
370
- grouped_inventory = defaultdict(list)
371
- for p in active_inventory:
372
- first_letter = p.get('name', '#')[0].upper()
373
- grouped_inventory[first_letter].append(p)
374
-
375
- sorted_grouped_inventory = sorted(grouped_inventory.items())
376
 
377
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
378
- return render_template_string(html, inventory=active_inventory, kassas=kassas, grouped_inventory=sorted_grouped_inventory, edit_tx=edit_tx)
379
 
380
  @app.route('/inventory', methods=['GET', 'POST'])
381
  def inventory_management():
@@ -461,7 +464,7 @@ def inventory_management():
461
 
462
  for product in inventory_list:
463
  if isinstance(product, dict) and 'variants' in product:
464
- for variant in product.get('variants', []):
465
  stock = variant.get('stock', 0)
466
  cost_price = to_decimal(variant.get('cost_price', '0'))
467
  price = to_decimal(variant.get('price_regular', variant.get('price', '0')))
@@ -588,7 +591,7 @@ def edit_product(product_id):
588
  def delete_product(product_id):
589
  inventory = load_json_data('inventory')
590
  initial_len = len(inventory)
591
- inventory = [p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
592
  if len(inventory) < initial_len:
593
  save_json_data('inventory', inventory)
594
  upload_db_to_hf('inventory')
@@ -620,7 +623,7 @@ def stock_in():
620
 
621
  variant_found = False
622
  variant_name_for_log = ""
623
- for i, variant in enumerate(product.get('variants', [])):
624
  if variant.get('id') == variant_id:
625
  variant_name_for_log = variant.get('option_value', '')
626
 
@@ -704,7 +707,7 @@ def get_product_by_barcode(barcode):
704
  inventory = load_json_data('inventory')
705
  product = find_item_by_field(inventory, 'barcode', barcode)
706
  if product:
707
- active_variants = [v for v in product.get('variants', []) if v.get('stock', 0) > 0]
708
  if active_variants:
709
  product_copy = product.copy()
710
  product_copy['variants'] = active_variants
@@ -734,11 +737,11 @@ def complete_sale():
734
  if not original_tx:
735
  return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
736
 
737
- for item in original_tx.get('items', []):
738
  if not item.get('is_custom'):
739
  for p in inventory:
740
  if p.get('id') == item.get('product_id'):
741
- for v in p.get('variants', []):
742
  if v.get('id') == item.get('variant_id'):
743
  v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
744
  break
@@ -750,7 +753,7 @@ def complete_sale():
750
  current_balance = to_decimal(k.get('balance', '0'))
751
  amount = to_decimal(original_tx.get('total_amount', '0'))
752
  k['balance'] = str(current_balance - amount)
753
- k.setdefault('history', []).append({
754
  'type': 'correction_revert',
755
  'amount': str(-amount),
756
  'timestamp': get_current_time().isoformat(),
@@ -774,7 +777,7 @@ def complete_sale():
774
  if not user or not kassa:
775
  return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
776
 
777
- sale_items = []
778
  items_total = Decimal('0')
779
  inventory_updates = {}
780
 
@@ -803,7 +806,7 @@ def complete_sale():
803
  if not product:
804
  return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
805
 
806
- variant = find_item_by_field(product.get('variants', []), 'id', variant_id)
807
  if not variant:
808
  return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
809
 
@@ -862,7 +865,7 @@ def complete_sale():
862
  }
863
 
864
  if edit_tx_id:
865
- new_transaction['edits'] = original_tx.get('edits', []) + [{'timestamp': now_iso, 'type': 'full_edit'}]
866
 
867
  new_transaction['invoice_html'] = generate_receipt_html(new_transaction)
868
 
@@ -877,7 +880,7 @@ def complete_sale():
877
  for variant_id, update_info in inventory_updates.items():
878
  for p in inventory:
879
  if p.get('id') == update_info['product_id']:
880
- for v in p.get('variants', []):
881
  if v.get('id') == variant_id:
882
  v['stock'] = update_info['new_stock']
883
  p['timestamp_updated'] = now_iso
@@ -950,7 +953,7 @@ def transaction_history():
950
  ]
951
 
952
  if selected_kassa_id:
953
- filtered_transactions = [
954
  t for t in filtered_transactions
955
  if t.get('kassa_id') == selected_kassa_id
956
  ]
@@ -960,7 +963,7 @@ def transaction_history():
960
  total_quantity_sold = 0
961
  for t in filtered_transactions:
962
  if t.get('type') == 'sale':
963
- for item in t.get('items', []):
964
  total_quantity_sold += int(item.get('quantity', 0))
965
 
966
  filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
@@ -973,7 +976,7 @@ def transaction_history():
973
  def edit_transaction(transaction_id):
974
  try:
975
  data = request.get_json()
976
- items_update = data.get('items', [])
977
 
978
  transactions = load_json_data('transactions')
979
  kassas = load_json_data('kassas')
@@ -1030,7 +1033,7 @@ def edit_transaction(transaction_id):
1030
  for i, k in enumerate(kassas):
1031
  if k.get('id') == kassa_id:
1032
  k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
1033
- k.setdefault('history', []).append({
1034
  'type': 'correction',
1035
  'amount': str(amount_diff),
1036
  'timestamp': get_current_time().isoformat(),
@@ -1061,14 +1064,14 @@ def delete_transaction(transaction_id):
1061
  flash("Транзакция не найдена.", "danger")
1062
  return redirect(url_for('transaction_history'))
1063
 
1064
- for item in transaction_to_delete.get('items', []):
1065
  if item.get('is_custom'):
1066
  continue
1067
 
1068
  product = find_item_by_field(inventory, 'id', item.get('product_id'))
1069
  if not product: continue
1070
 
1071
- variant = find_item_by_field(product.get('variants', []), 'id', item.get('variant_id'))
1072
  if not variant: continue
1073
 
1074
  quantity_change = item.get('quantity', 0)
@@ -1086,14 +1089,14 @@ def delete_transaction(transaction_id):
1086
 
1087
  kassa['balance'] = str(current_balance - amount_change)
1088
 
1089
- kassa.setdefault('history', []).append({
1090
  'type': 'deletion',
1091
  'amount': str(-amount_change),
1092
  'timestamp': get_current_time().isoformat(),
1093
  'description': f"Удаление транзакции {transaction_id[:8]}"
1094
  })
1095
 
1096
- transactions = [t for t in transactions if t.get('id') != transaction_id]
1097
 
1098
  if transaction_to_delete.get('type') == 'return':
1099
  original_id = transaction_to_delete.get('original_transaction_id')
@@ -1130,20 +1133,20 @@ def reports():
1130
  personal_expenses = load_json_data('personal_expenses')
1131
  users = load_json_data('users')
1132
 
1133
- filtered_transactions = [
1134
  t for t in transactions
1135
  if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
1136
  ]
1137
- filtered_expenses = [
1138
  e for e in expenses
1139
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1140
  ]
1141
- filtered_personal_expenses = [
1142
  e for e in personal_expenses
1143
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1144
  ]
1145
 
1146
- return_transactions = [t for t in filtered_transactions if t.get('type') == 'return']
1147
  total_returns_amount = sum(to_decimal(t['total_amount']) for t in return_transactions)
1148
  total_returns_count = len(return_transactions)
1149
 
@@ -1222,7 +1225,7 @@ def employee_report():
1222
  end_date = (datetime.strptime(end_date_str, '%Y-%m-%d') + timedelta(days=1)).replace(tzinfo=ALMATY_TZ)
1223
 
1224
  transactions = load_json_data('transactions')
1225
- employee_transactions = []
1226
  totals = {'sales': Decimal(0), 'returns': Decimal(0), 'net': Decimal(0)}
1227
 
1228
  if user_id:
@@ -1254,17 +1257,17 @@ def item_movement_report():
1254
  product_id = request.args.get('product_id', '')
1255
  variant_id = request.args.get('variant_id', '')
1256
 
1257
- movements = []
1258
  selected_product = None
1259
  selected_variant = None
1260
 
1261
  if product_id:
1262
  selected_product = find_item_by_field(inventory, 'id', product_id)
1263
  if selected_product and variant_id:
1264
- selected_variant = find_item_by_field(selected_product.get('variants', []), 'id', variant_id)
1265
 
1266
  for t in transactions:
1267
- for item in t.get('items', []):
1268
  if item.get('product_id') == product_id and (not variant_id or item.get('variant_id') == variant_id):
1269
  movements.append({
1270
  'timestamp': t['timestamp'],
@@ -1302,13 +1305,13 @@ def product_roi_report():
1302
  product_stats = []
1303
 
1304
  for product in inventory:
1305
- for variant in product.get('variants', []):
1306
  total_revenue = Decimal('0')
1307
  total_cogs = Decimal('0')
1308
  total_qty_sold = 0
1309
 
1310
  for t in transactions:
1311
- if t['type'] in ['sale', 'return']:
1312
  for item in t['items']:
1313
  if item.get('variant_id') == variant['id']:
1314
  total_revenue += to_decimal(item['total'])
@@ -1438,7 +1441,7 @@ def manage_kassa():
1438
  elif action == 'delete':
1439
  kassa_id = request.form.get('id')
1440
  initial_len = len(kassas)
1441
- kassas = [k for k in kassas if k.get('id') != kassa_id]
1442
  if len(kassas) < initial_len:
1443
  flash("Касса удалена.", "success")
1444
  else:
@@ -1476,7 +1479,7 @@ def kassa_operation():
1476
  new_balance -= amount
1477
 
1478
  kassas[i]['balance'] = str(new_balance)
1479
- if 'history' not in kassas[i]: kassas[i]['history'] = []
1480
  kassas[i]['history'].append({
1481
  'type': op_type,
1482
  'amount': str(amount),
@@ -1560,7 +1563,7 @@ def manage_personal_expense():
1560
  def delete_personal_expense(expense_id):
1561
  expenses = load_json_data('personal_expenses')
1562
  initial_len = len(expenses)
1563
- expenses = [e for e in expenses if e.get('id') != expense_id]
1564
  if len(expenses) < initial_len:
1565
  save_json_data('personal_expenses', expenses)
1566
  upload_db_to_hf('personal_expenses')
@@ -1582,7 +1585,7 @@ def manage_link():
1582
  flash("Ссылка добавлена.", "success")
1583
  elif action == 'delete':
1584
  link_id = request.form.get('id')
1585
- links = [l for l in links if l.get('id') != link_id]
1586
  flash("Ссылка удалена.", "success")
1587
  save_json_data('links', links)
1588
  upload_db_to_hf('links')
@@ -1679,7 +1682,7 @@ def end_shift():
1679
 
1680
  shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
1681
 
1682
- shift_transactions = [
1683
  t for t in transactions
1684
  if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
1685
  ]
@@ -1740,7 +1743,7 @@ def return_transaction(transaction_id):
1740
  flash("Не указан ID кассира.", "danger")
1741
  return redirect(url_for('cashier_login'))
1742
 
1743
- returnable_items = []
1744
  already_returned = original_transaction.get('return_info', {}).get('returned_items', {})
1745
 
1746
  for item in original_transaction['items']:
@@ -1761,7 +1764,7 @@ def return_transaction(transaction_id):
1761
  flash("Не удалось определить кассира.", "danger")
1762
  return redirect(url_for('cashier_login'))
1763
 
1764
- return_items = []
1765
  total_return_amount = Decimal('0')
1766
  inventory_updates = {}
1767
  items_to_process = defaultdict(int)
@@ -1806,7 +1809,7 @@ def return_transaction(transaction_id):
1806
  if not item.get('is_custom'):
1807
  product = find_item_by_field(inventory, 'id', item['product_id'])
1808
  if product:
1809
- variant = find_item_by_field(product.get('variants', []), 'id', variant_id)
1810
  if variant:
1811
  inventory_updates[variant_id] = {'product_id': item['product_id'], 'stock_change': qty_to_return}
1812
 
@@ -1841,7 +1844,7 @@ def return_transaction(transaction_id):
1841
  for variant_id, update_info in inventory_updates.items():
1842
  for p_idx, p in enumerate(inventory):
1843
  if p.get('id') == update_info['product_id']:
1844
- for v_idx, v in enumerate(p.get('variants', [])):
1845
  if v.get('id') == variant_id:
1846
  inventory[p_idx]['variants'][v_idx]['stock'] += update_info['stock_change']
1847
  inventory[p_idx]['timestamp_updated'] = now_iso
@@ -1853,7 +1856,7 @@ def return_transaction(transaction_id):
1853
  if k.get('id') == original_transaction['kassa_id']:
1854
  current_balance = to_decimal(k.get('balance', '0'))
1855
  kassas[k_idx]['balance'] = str(current_balance - total_return_amount)
1856
- kassas[k_idx].setdefault('history', []).append({
1857
  'type': 'return', 'amount': str(-total_return_amount), 'timestamp': now_iso,
1858
  'transaction_id': return_transaction_id
1859
  })
@@ -1883,7 +1886,7 @@ def backup_hf():
1883
  @app.route('/download', methods=['GET'])
1884
  @admin_required
1885
  def download_hf():
1886
- errors = []
1887
  success_count = 0
1888
  for key in DATA_FILES.keys():
1889
  filepath, _ = DATA_FILES[key]
@@ -1963,7 +1966,7 @@ def restore_bill(bill_id):
1963
  def delete_held_bill(bill_id):
1964
  bills = load_json_data('held_bills')
1965
  initial_len = len(bills)
1966
- bills = [b for b in bills if b.get('id') != bill_id]
1967
  if len(bills) < initial_len:
1968
  save_json_data('held_bills', bills)
1969
  upload_db_to_hf('held_bills')
@@ -2001,7 +2004,7 @@ def manage_customer():
2001
  elif action == 'delete':
2002
  customer_id = request.form.get('id')
2003
  initial_len = len(customers)
2004
- customers = [c for c in customers if c.get('id') != customer_id]
2005
  if len(customers) < initial_len:
2006
  flash("Клиент удален.", "success")
2007
  else:
@@ -2017,7 +2020,7 @@ def search_customers():
2017
  if not query:
2018
  return jsonify([])
2019
  customers = load_json_data('customers')
2020
- matches = [
2021
  c for c in customers
2022
  if query in c.get('name', '').lower() or query in c.get('phone', '')
2023
  ]
@@ -2063,13 +2066,10 @@ BASE_TEMPLATE = """
2063
  .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
2064
  .sidebar.active { transform: translateX(0); }
2065
  .main-content { margin-left: 0; }
2066
- }
2067
- [data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
2068
- [data-bs-theme="dark"] .card, [data-bs-theme="dark"] .modal-content, [data-bs-theme="dark"] .list-group-item, [data-bs-theme="dark"] .table, [data-bs-theme="dark"] .accordion-item { background-color: #343a40; }
2069
- [data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }
2070
  [data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
2071
- [data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }
2072
- [data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
2073
  [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
2074
  .product-card { cursor: pointer; }
2075
  .product-card:hover { border-color: var(--bs-primary); }
@@ -2100,7 +2100,7 @@ BASE_TEMPLATE = """
2100
  </li>
2101
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'customer_management' %}active{% endif %}" href="{{ url_for('customer_management') }}"><i class="fas fa-fw fa-users me-2"></i>Клиенты</a></li>
2102
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['cashier_login', 'cashier_dashboard'] %}active{% endif %}" href="{{ url_for('cashier_login') }}"><i class="fas fa-fw fa-user-circle me-2"></i>Кабинет кассира</a></li>
2103
- <li class="nav-item"><a class="nav-link {% if request.endpoint in ['admin_panel', 'admin_shifts'] %}active{% endif %}" href="{{ url_for('admin_panel') }}"><i class="fas fa-fw fa-cogs me-2"></i>Админка</a></li>
2104
 
2105
  {% if not session.admin_logged_in %}
2106
  <li class="nav-item mt-3"><a class="nav-link text-warning" href="{{ url_for('admin_login') }}"><i class="fas fa-fw fa-user-shield me-2"></i>Войти как админ</a></li>
@@ -2339,36 +2339,33 @@ SALES_SCREEN_CONTENT = """
2339
  <div id="scanner-status" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; display: none;"></div>
2340
  <button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
2341
  </div>
2342
- <input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
 
 
 
 
 
 
 
 
 
 
2343
  <div id="product-search-results" class="d-grid gap-2" style="display: none; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
2344
 
2345
- <div id="product-accordion" class="accordion">
2346
- {% for letter, products in grouped_inventory %}
2347
- <div class="accordion-item">
2348
- <h2 class="accordion-header" id="heading-{{ letter }}">
2349
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ letter }}" aria-expanded="false" aria-controls="collapse-{{ letter }}">
2350
- {{ letter }}
2351
- </button>
2352
- </h2>
2353
- <div id="collapse-{{ letter }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ letter }}" data-bs-parent="#product-accordion">
2354
- <div class="accordion-body d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
2355
- {% for p in products %}
2356
- <div class="card text-center product-card" data-barcode="{{ p.barcode }}">
2357
- <div class="card-body p-2">
2358
- <h6 class="card-title small mb-1">{{ p.name }}</h6>
2359
- <p class="card-text fw-bold mb-0">
2360
- {% if p.variants|length > 1 %}
2361
- от {{ format_currency_py(p.variants|map(attribute='price_regular')|min) }} ₸
2362
- {% elif p.variants|length == 1 %}
2363
- {{ format_currency_py(p.variants[0].get('price_regular', p.variants[0].get('price'))) }} ₸
2364
- {% else %}
2365
- Нет в наличии
2366
- {% endif %}
2367
- </p>
2368
- </div>
2369
- </div>
2370
- {% endfor %}
2371
- </div>
2372
  </div>
2373
  </div>
2374
  {% endfor %}
@@ -2392,6 +2389,7 @@ SALES_SCREEN_CONTENT = """
2392
  </div>
2393
  <div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
2394
  <div class="mb-3">
 
2395
  <div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
2396
  <div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
2397
  <hr class="my-1">
@@ -2503,6 +2501,7 @@ document.addEventListener('DOMContentLoaded', () => {
2503
  const cartSubtotalEl = document.getElementById('cart-subtotal');
2504
  const cartDeliveryEl = document.getElementById('cart-delivery');
2505
  const cartTotalEl = document.getElementById('cart-total');
 
2506
 
2507
  let audioCtx;
2508
  let isScannerPaused = false;
@@ -2585,12 +2584,14 @@ document.addEventListener('DOMContentLoaded', () => {
2585
  const updateCartView = () => {
2586
  cartItemsEl.innerHTML = '';
2587
  let subtotal = 0;
 
2588
  if (Object.keys(cart).length === 0) {
2589
  cartItemsEl.innerHTML = '<p class="text-center text-muted">Корзина пуста</p>';
2590
  }
2591
  for (const id in cart) {
2592
  const item = cart[id];
2593
  subtotal += (parseLocaleNumber(item.price) - parseLocaleNumber(item.discount)) * item.quantity;
 
2594
 
2595
  let nameField = '';
2596
  let priceField = '';
@@ -2634,6 +2635,7 @@ document.addEventListener('DOMContentLoaded', () => {
2634
  cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
2635
  cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
2636
  cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
 
2637
  };
2638
 
2639
  const addToCart = (product, variant, price, priceType) => {
@@ -2659,7 +2661,7 @@ document.addEventListener('DOMContentLoaded', () => {
2659
  const container = document.getElementById('price-options-container');
2660
  container.innerHTML = '';
2661
 
2662
- const prices = [
2663
  { type: 'Общая', value: variant.price_regular || variant.price },
2664
  { type: 'Минимальная', value: variant.price_min },
2665
  { type: 'Оптовая', value: variant.price_wholesale }
@@ -2799,24 +2801,49 @@ document.addEventListener('DOMContentLoaded', () => {
2799
  });
2800
  }
2801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2802
  const productSearchInput = document.getElementById('product-search');
2803
- const productAccordionEl = document.getElementById('product-accordion');
 
2804
  const productSearchResultsEl = document.getElementById('product-search-results');
2805
 
2806
  productSearchInput.addEventListener('input', e => {
2807
  const term = e.target.value.toLowerCase().trim();
2808
 
2809
  if (term === '') {
2810
- productAccordionEl.style.display = '';
 
2811
  productSearchResultsEl.style.display = 'none';
2812
- productSearchResultsEl.innerHTML = '';
2813
- document.querySelectorAll('#product-accordion .accordion-collapse.show').forEach(el => {
2814
- bootstrap.Collapse.getOrCreateInstance(el).hide();
2815
- });
2816
  return;
2817
  }
2818
 
2819
- productAccordionEl.style.display = 'none';
 
2820
  productSearchResultsEl.style.display = 'grid';
2821
 
2822
  const filtered = allProducts.filter(p => p.name.toLowerCase().includes(term) || p.barcode.toLowerCase().includes(term));
@@ -4452,7 +4479,8 @@ ADMIN_CONTENT = """
4452
  <td class="text-end fw-bold">{{ format_currency_py(e.amount) }} ₸</td>
4453
  <td class="text-end">
4454
  <form action="{{ url_for('delete_personal_expense', expense_id=e.id) }}" method="POST" onsubmit="return confirm('Удалить этот расход?');">
4455
- <button type="submit" class="btn btn-sm btn-outline-danger py-0 px-1"><i class="fas fa-trash"></i></button>
 
4456
  </form>
4457
  </td>
4458
  </tr>
@@ -4626,7 +4654,7 @@ CASHIER_DASHBOARD_CONTENT = """
4626
  {% endif %}
4627
  </td>
4628
  <td>
4629
- {% if t.type == 'sale' and t.status in ['completed', 'partially_returned'] %}
4630
  <a href="{{ url_for('return_transaction', transaction_id=t.id, cashier_id=user.id) }}" class="btn btn-sm btn-warning">Возврат</a>
4631
  {% endif %}
4632
  </td>
@@ -4731,7 +4759,7 @@ CUSTOMERS_CONTENT = """
4731
  </div>
4732
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Сохранить</button></div>
4733
  </form>
4734
- </div>
4735
  </div>
4736
  </div>
4737
  """
 
95
  with open(filepath, 'r', encoding='utf-8') as f:
96
  return json.load(f)
97
  except (FileNotFoundError, json.JSONDecodeError):
98
+ return[]
99
 
100
  def save_json_data(file_key, data):
101
  filepath, lock = DATA_FILES[file_key]
 
162
  return "0"
163
 
164
  def generate_receipt_html(transaction):
165
+ has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items',[]))
166
  delivery_cost = to_decimal(transaction.get('delivery_cost', '0'))
167
  note = transaction.get('note', '')
168
 
169
  if has_discounts:
170
  table_headers = """
171
+ <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
172
+ <th style="text-align: left; white-space: nowrap;">Наименование</th>
173
+ <th style="text-align: right; white-space: nowrap;">Кол-во</th>
174
+ <th style="text-align: right; white-space: nowrap;">Цена</th>
175
+ <th style="text-align: right; white-space: nowrap;">Скидка</th>
176
+ <th style="text-align: right; white-space: nowrap;">Сумма</th>
177
  """
178
  total_colspan = 5
179
  else:
180
  table_headers = """
181
+ <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
182
+ <th style="text-align: left; white-space: nowrap;">Наименование</th>
183
+ <th style="text-align: right; white-space: nowrap;">Кол-во</th>
184
+ <th style="text-align: right; white-space: nowrap;">Цена</th>
185
+ <th style="text-align: right; white-space: nowrap;">Сумма</th>
186
  """
187
  total_colspan = 4
188
 
189
  items_html = ""
190
+ total_quantity = 0
191
  for i, item in enumerate(transaction['items']):
192
+ total_quantity += int(item.get('quantity', 0))
193
+ discount_cell = f"""<td style="text-align: right; white-space: nowrap;">{format_currency_py(item.get('discount_per_item', '0'))}</td>""" if has_discounts else ""
194
  items_html += f"""
195
  <tr>
196
+ <td style="text-align: center; white-space: nowrap;">{i + 1}</td>
197
+ <td style="white-space: nowrap;">{item['name']}</td>
198
+ <td style="text-align: right; white-space: nowrap;">{item['quantity']}</td>
199
+ <td style="text-align: right; white-space: nowrap;">{format_currency_py(item['price_at_sale'])}</td>
200
  {discount_cell}
201
+ <td style="text-align: right; white-space: nowrap;">{format_currency_py(item['total'])}</td>
202
  </tr>
203
  """
204
 
205
  total_amount_from_db = to_decimal(transaction['total_amount'])
206
 
207
  totals_html = ""
208
+ totals_html += f"""
209
+ <tr>
210
+ <td colspan="{total_colspan}" style="text-align: right; white-space: nowrap;">Общее кол-во единиц:</td>
211
+ <td style="text-align: right; white-space: nowrap;">{total_quantity}</td>
212
+ </tr>
213
+ """
214
+
215
  if delivery_cost > 0:
216
  subtotal = total_amount_from_db - delivery_cost
217
  totals_html += f"""
218
  <tr>
219
+ <td colspan="{total_colspan}" style="text-align: right; white-space: nowrap;">Подытог:</td>
220
+ <td style="text-align: right; white-space: nowrap;">{format_currency_py(subtotal)} ₸</td>
221
  </tr>
222
  <tr>
223
+ <td colspan="{total_colspan}" style="text-align: right; white-space: nowrap;">Доставка:</td>
224
+ <td style="text-align: right; white-space: nowrap;">{format_currency_py(delivery_cost)} ₸</td>
225
  </tr>
226
  """
227
 
228
  totals_html += f"""
229
  <tr class="total">
230
+ <td colspan="{total_colspan}" style="text-align: right; white-space: nowrap;">Итого к оплате:</td>
231
+ <td style="text-align: right; white-space: nowrap;">{format_currency_py(total_amount_from_db)} ₸</td>
232
  </tr>
233
  """
234
 
 
265
  .header p {{ margin: 2px 0; font-size: 14px; }}
266
  .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
267
  table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
268
+ table th {{ background: #f2f2f2; font-weight: bold; padding: 8px; border-bottom: 2px solid #ddd; white-space: nowrap; }}
269
+ table td {{ padding: 8px; border-bottom: 1px solid #eee; white-space: nowrap; }}
270
  table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
271
  .footer-info {{ font-size: 14px; margin-top: 20px; }}
272
  .print-hide {{ display: block; }}
 
369
  transactions = load_json_data('transactions')
370
  edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
371
 
372
+ active_inventory =[]
373
  for p in inventory:
374
+ if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',[])):
375
  active_inventory.append(p)
376
 
377
  active_inventory.sort(key=lambda x: x.get('name', '').lower())
378
+ letters = sorted(list(set([p.get('name', '#')[0].upper() for p in active_inventory])))
 
 
 
 
 
 
379
 
380
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
381
+ return render_template_string(html, inventory=active_inventory, kassas=kassas, letters=letters, edit_tx=edit_tx)
382
 
383
  @app.route('/inventory', methods=['GET', 'POST'])
384
  def inventory_management():
 
464
 
465
  for product in inventory_list:
466
  if isinstance(product, dict) and 'variants' in product:
467
+ for variant in product.get('variants',[]):
468
  stock = variant.get('stock', 0)
469
  cost_price = to_decimal(variant.get('cost_price', '0'))
470
  price = to_decimal(variant.get('price_regular', variant.get('price', '0')))
 
591
  def delete_product(product_id):
592
  inventory = load_json_data('inventory')
593
  initial_len = len(inventory)
594
+ inventory =[p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
595
  if len(inventory) < initial_len:
596
  save_json_data('inventory', inventory)
597
  upload_db_to_hf('inventory')
 
623
 
624
  variant_found = False
625
  variant_name_for_log = ""
626
+ for i, variant in enumerate(product.get('variants',[])):
627
  if variant.get('id') == variant_id:
628
  variant_name_for_log = variant.get('option_value', '')
629
 
 
707
  inventory = load_json_data('inventory')
708
  product = find_item_by_field(inventory, 'barcode', barcode)
709
  if product:
710
+ active_variants =[v for v in product.get('variants', []) if v.get('stock', 0) > 0]
711
  if active_variants:
712
  product_copy = product.copy()
713
  product_copy['variants'] = active_variants
 
737
  if not original_tx:
738
  return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
739
 
740
+ for item in original_tx.get('items',[]):
741
  if not item.get('is_custom'):
742
  for p in inventory:
743
  if p.get('id') == item.get('product_id'):
744
+ for v in p.get('variants',[]):
745
  if v.get('id') == item.get('variant_id'):
746
  v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
747
  break
 
753
  current_balance = to_decimal(k.get('balance', '0'))
754
  amount = to_decimal(original_tx.get('total_amount', '0'))
755
  k['balance'] = str(current_balance - amount)
756
+ k.setdefault('history',[]).append({
757
  'type': 'correction_revert',
758
  'amount': str(-amount),
759
  'timestamp': get_current_time().isoformat(),
 
777
  if not user or not kassa:
778
  return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
779
 
780
+ sale_items =[]
781
  items_total = Decimal('0')
782
  inventory_updates = {}
783
 
 
806
  if not product:
807
  return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
808
 
809
+ variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
810
  if not variant:
811
  return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
812
 
 
865
  }
866
 
867
  if edit_tx_id:
868
+ new_transaction['edits'] = original_tx.get('edits', []) +[{'timestamp': now_iso, 'type': 'full_edit'}]
869
 
870
  new_transaction['invoice_html'] = generate_receipt_html(new_transaction)
871
 
 
880
  for variant_id, update_info in inventory_updates.items():
881
  for p in inventory:
882
  if p.get('id') == update_info['product_id']:
883
+ for v in p.get('variants',[]):
884
  if v.get('id') == variant_id:
885
  v['stock'] = update_info['new_stock']
886
  p['timestamp_updated'] = now_iso
 
953
  ]
954
 
955
  if selected_kassa_id:
956
+ filtered_transactions =[
957
  t for t in filtered_transactions
958
  if t.get('kassa_id') == selected_kassa_id
959
  ]
 
963
  total_quantity_sold = 0
964
  for t in filtered_transactions:
965
  if t.get('type') == 'sale':
966
+ for item in t.get('items',[]):
967
  total_quantity_sold += int(item.get('quantity', 0))
968
 
969
  filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
 
976
  def edit_transaction(transaction_id):
977
  try:
978
  data = request.get_json()
979
+ items_update = data.get('items',[])
980
 
981
  transactions = load_json_data('transactions')
982
  kassas = load_json_data('kassas')
 
1033
  for i, k in enumerate(kassas):
1034
  if k.get('id') == kassa_id:
1035
  k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
1036
+ k.setdefault('history',[]).append({
1037
  'type': 'correction',
1038
  'amount': str(amount_diff),
1039
  'timestamp': get_current_time().isoformat(),
 
1064
  flash("Транзакция не найдена.", "danger")
1065
  return redirect(url_for('transaction_history'))
1066
 
1067
+ for item in transaction_to_delete.get('items',[]):
1068
  if item.get('is_custom'):
1069
  continue
1070
 
1071
  product = find_item_by_field(inventory, 'id', item.get('product_id'))
1072
  if not product: continue
1073
 
1074
+ variant = find_item_by_field(product.get('variants',[]), 'id', item.get('variant_id'))
1075
  if not variant: continue
1076
 
1077
  quantity_change = item.get('quantity', 0)
 
1089
 
1090
  kassa['balance'] = str(current_balance - amount_change)
1091
 
1092
+ kassa.setdefault('history',[]).append({
1093
  'type': 'deletion',
1094
  'amount': str(-amount_change),
1095
  'timestamp': get_current_time().isoformat(),
1096
  'description': f"Удаление транзакции {transaction_id[:8]}"
1097
  })
1098
 
1099
+ transactions =[t for t in transactions if t.get('id') != transaction_id]
1100
 
1101
  if transaction_to_delete.get('type') == 'return':
1102
  original_id = transaction_to_delete.get('original_transaction_id')
 
1133
  personal_expenses = load_json_data('personal_expenses')
1134
  users = load_json_data('users')
1135
 
1136
+ filtered_transactions =[
1137
  t for t in transactions
1138
  if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
1139
  ]
1140
+ filtered_expenses =[
1141
  e for e in expenses
1142
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1143
  ]
1144
+ filtered_personal_expenses =[
1145
  e for e in personal_expenses
1146
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1147
  ]
1148
 
1149
+ return_transactions =[t for t in filtered_transactions if t.get('type') == 'return']
1150
  total_returns_amount = sum(to_decimal(t['total_amount']) for t in return_transactions)
1151
  total_returns_count = len(return_transactions)
1152
 
 
1225
  end_date = (datetime.strptime(end_date_str, '%Y-%m-%d') + timedelta(days=1)).replace(tzinfo=ALMATY_TZ)
1226
 
1227
  transactions = load_json_data('transactions')
1228
+ employee_transactions =[]
1229
  totals = {'sales': Decimal(0), 'returns': Decimal(0), 'net': Decimal(0)}
1230
 
1231
  if user_id:
 
1257
  product_id = request.args.get('product_id', '')
1258
  variant_id = request.args.get('variant_id', '')
1259
 
1260
+ movements =[]
1261
  selected_product = None
1262
  selected_variant = None
1263
 
1264
  if product_id:
1265
  selected_product = find_item_by_field(inventory, 'id', product_id)
1266
  if selected_product and variant_id:
1267
+ selected_variant = find_item_by_field(selected_product.get('variants',[]), 'id', variant_id)
1268
 
1269
  for t in transactions:
1270
+ for item in t.get('items',[]):
1271
  if item.get('product_id') == product_id and (not variant_id or item.get('variant_id') == variant_id):
1272
  movements.append({
1273
  'timestamp': t['timestamp'],
 
1305
  product_stats = []
1306
 
1307
  for product in inventory:
1308
+ for variant in product.get('variants',[]):
1309
  total_revenue = Decimal('0')
1310
  total_cogs = Decimal('0')
1311
  total_qty_sold = 0
1312
 
1313
  for t in transactions:
1314
+ if t['type'] in['sale', 'return']:
1315
  for item in t['items']:
1316
  if item.get('variant_id') == variant['id']:
1317
  total_revenue += to_decimal(item['total'])
 
1441
  elif action == 'delete':
1442
  kassa_id = request.form.get('id')
1443
  initial_len = len(kassas)
1444
+ kassas =[k for k in kassas if k.get('id') != kassa_id]
1445
  if len(kassas) < initial_len:
1446
  flash("Касса удалена.", "success")
1447
  else:
 
1479
  new_balance -= amount
1480
 
1481
  kassas[i]['balance'] = str(new_balance)
1482
+ if 'history' not in kassas[i]: kassas[i]['history'] =[]
1483
  kassas[i]['history'].append({
1484
  'type': op_type,
1485
  'amount': str(amount),
 
1563
  def delete_personal_expense(expense_id):
1564
  expenses = load_json_data('personal_expenses')
1565
  initial_len = len(expenses)
1566
+ expenses =[e for e in expenses if e.get('id') != expense_id]
1567
  if len(expenses) < initial_len:
1568
  save_json_data('personal_expenses', expenses)
1569
  upload_db_to_hf('personal_expenses')
 
1585
  flash("Ссылка добавлена.", "success")
1586
  elif action == 'delete':
1587
  link_id = request.form.get('id')
1588
+ links =[l for l in links if l.get('id') != link_id]
1589
  flash("Ссылка удалена.", "success")
1590
  save_json_data('links', links)
1591
  upload_db_to_hf('links')
 
1682
 
1683
  shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
1684
 
1685
+ shift_transactions =[
1686
  t for t in transactions
1687
  if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
1688
  ]
 
1743
  flash("Не указан ID кассира.", "danger")
1744
  return redirect(url_for('cashier_login'))
1745
 
1746
+ returnable_items =[]
1747
  already_returned = original_transaction.get('return_info', {}).get('returned_items', {})
1748
 
1749
  for item in original_transaction['items']:
 
1764
  flash("Не удалось определить кассира.", "danger")
1765
  return redirect(url_for('cashier_login'))
1766
 
1767
+ return_items =[]
1768
  total_return_amount = Decimal('0')
1769
  inventory_updates = {}
1770
  items_to_process = defaultdict(int)
 
1809
  if not item.get('is_custom'):
1810
  product = find_item_by_field(inventory, 'id', item['product_id'])
1811
  if product:
1812
+ variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
1813
  if variant:
1814
  inventory_updates[variant_id] = {'product_id': item['product_id'], 'stock_change': qty_to_return}
1815
 
 
1844
  for variant_id, update_info in inventory_updates.items():
1845
  for p_idx, p in enumerate(inventory):
1846
  if p.get('id') == update_info['product_id']:
1847
+ for v_idx, v in enumerate(p.get('variants',[])):
1848
  if v.get('id') == variant_id:
1849
  inventory[p_idx]['variants'][v_idx]['stock'] += update_info['stock_change']
1850
  inventory[p_idx]['timestamp_updated'] = now_iso
 
1856
  if k.get('id') == original_transaction['kassa_id']:
1857
  current_balance = to_decimal(k.get('balance', '0'))
1858
  kassas[k_idx]['balance'] = str(current_balance - total_return_amount)
1859
+ kassas[k_idx].setdefault('history',[]).append({
1860
  'type': 'return', 'amount': str(-total_return_amount), 'timestamp': now_iso,
1861
  'transaction_id': return_transaction_id
1862
  })
 
1886
  @app.route('/download', methods=['GET'])
1887
  @admin_required
1888
  def download_hf():
1889
+ errors =[]
1890
  success_count = 0
1891
  for key in DATA_FILES.keys():
1892
  filepath, _ = DATA_FILES[key]
 
1966
  def delete_held_bill(bill_id):
1967
  bills = load_json_data('held_bills')
1968
  initial_len = len(bills)
1969
+ bills =[b for b in bills if b.get('id') != bill_id]
1970
  if len(bills) < initial_len:
1971
  save_json_data('held_bills', bills)
1972
  upload_db_to_hf('held_bills')
 
2004
  elif action == 'delete':
2005
  customer_id = request.form.get('id')
2006
  initial_len = len(customers)
2007
+ customers =[c for c in customers if c.get('id') != customer_id]
2008
  if len(customers) < initial_len:
2009
  flash("Клиент удален.", "success")
2010
  else:
 
2020
  if not query:
2021
  return jsonify([])
2022
  customers = load_json_data('customers')
2023
+ matches =[
2024
  c for c in customers
2025
  if query in c.get('name', '').lower() or query in c.get('phone', '')
2026
  ]
 
2066
  .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
2067
  .sidebar.active { transform: translateX(0); }
2068
  .main-content { margin-left: 0; }
2069
+ }[data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
2070
+ [data-bs-theme="dark"] .card,[data-bs-theme="dark"] .modal-content, [data-bs-theme="dark"] .list-group-item, [data-bs-theme="dark"] .table, [data-bs-theme="dark"] .accordion-item { background-color: #343a40; }[data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }
 
 
2071
  [data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
2072
+ [data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }[data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
 
2073
  [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
2074
  .product-card { cursor: pointer; }
2075
  .product-card:hover { border-color: var(--bs-primary); }
 
2100
  </li>
2101
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'customer_management' %}active{% endif %}" href="{{ url_for('customer_management') }}"><i class="fas fa-fw fa-users me-2"></i>Клиенты</a></li>
2102
  <li class="nav-item"><a class="nav-link {% if request.endpoint in ['cashier_login', 'cashier_dashboard'] %}active{% endif %}" href="{{ url_for('cashier_login') }}"><i class="fas fa-fw fa-user-circle me-2"></i>Кабинет кассира</a></li>
2103
+ <li class="nav-item"><a class="nav-link {% if request.endpoint in['admin_panel', 'admin_shifts'] %}active{% endif %}" href="{{ url_for('admin_panel') }}"><i class="fas fa-fw fa-cogs me-2"></i>Админка</a></li>
2104
 
2105
  {% if not session.admin_logged_in %}
2106
  <li class="nav-item mt-3"><a class="nav-link text-warning" href="{{ url_for('admin_login') }}"><i class="fas fa-fw fa-user-shield me-2"></i>Войти как админ</a></li>
 
2339
  <div id="scanner-status" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; display: none;"></div>
2340
  <button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
2341
  </div>
2342
+
2343
+ <div class="d-flex flex-column flex-md-row justify-content-between align-items-start mb-3 gap-2">
2344
+ <input type="text" id="product-search" class="form-control flex-grow-1" style="max-width: 400px;" placeholder="Поиск по названию или штрих-коду...">
2345
+ <div class="d-flex flex-wrap gap-1 justify-content-md-end" id="alphabet-filter">
2346
+ <button class="btn btn-secondary btn-sm filter-btn active" data-letter="all">Все</button>
2347
+ {% for letter in letters %}
2348
+ <button class="btn btn-outline-secondary btn-sm filter-btn" data-letter="{{ letter }}">{{ letter }}</button>
2349
+ {% endfor %}
2350
+ </div>
2351
+ </div>
2352
+
2353
  <div id="product-search-results" class="d-grid gap-2" style="display: none; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
2354
 
2355
+ <div id="product-grid" class="d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
2356
+ {% for p in inventory %}
2357
+ <div class="card text-center product-card" data-barcode="{{ p.barcode }}" data-letter="{{ p.name[0]|upper }}">
2358
+ <div class="card-body p-2">
2359
+ <h6 class="card-title small mb-1">{{ p.name }}</h6>
2360
+ <p class="card-text fw-bold mb-0">
2361
+ {% if p.variants|length > 1 %}
2362
+ от {{ format_currency_py(p.variants|map(attribute='price_regular')|min) }} ₸
2363
+ {% elif p.variants|length == 1 %}
2364
+ {{ format_currency_py(p.variants[0].get('price_regular', p.variants[0].get('price'))) }} ₸
2365
+ {% else %}
2366
+ Нет в наличии
2367
+ {% endif %}
2368
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
2369
  </div>
2370
  </div>
2371
  {% endfor %}
 
2389
  </div>
2390
  <div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
2391
  <div class="mb-3">
2392
+ <div class="d-flex justify-content-between"><span>Общее кол-во единиц:</span><span id="cart-total-qty">0 шт.</span></div>
2393
  <div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
2394
  <div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
2395
  <hr class="my-1">
 
2501
  const cartSubtotalEl = document.getElementById('cart-subtotal');
2502
  const cartDeliveryEl = document.getElementById('cart-delivery');
2503
  const cartTotalEl = document.getElementById('cart-total');
2504
+ const cartTotalQtyEl = document.getElementById('cart-total-qty');
2505
 
2506
  let audioCtx;
2507
  let isScannerPaused = false;
 
2584
  const updateCartView = () => {
2585
  cartItemsEl.innerHTML = '';
2586
  let subtotal = 0;
2587
+ let totalQty = 0;
2588
  if (Object.keys(cart).length === 0) {
2589
  cartItemsEl.innerHTML = '<p class="text-center text-muted">Корзина пуста</p>';
2590
  }
2591
  for (const id in cart) {
2592
  const item = cart[id];
2593
  subtotal += (parseLocaleNumber(item.price) - parseLocaleNumber(item.discount)) * item.quantity;
2594
+ totalQty += parseInt(item.quantity, 10) || 0;
2595
 
2596
  let nameField = '';
2597
  let priceField = '';
 
2635
  cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
2636
  cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
2637
  cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
2638
+ if(cartTotalQtyEl) cartTotalQtyEl.textContent = totalQty + ' шт.';
2639
  };
2640
 
2641
  const addToCart = (product, variant, price, priceType) => {
 
2661
  const container = document.getElementById('price-options-container');
2662
  container.innerHTML = '';
2663
 
2664
+ const prices =[
2665
  { type: 'Общая', value: variant.price_regular || variant.price },
2666
  { type: 'Минимальная', value: variant.price_min },
2667
  { type: 'Оптовая', value: variant.price_wholesale }
 
2801
  });
2802
  }
2803
 
2804
+ document.getElementById('alphabet-filter')?.addEventListener('click', e => {
2805
+ if (e.target.classList.contains('filter-btn')) {
2806
+ document.querySelectorAll('.filter-btn').forEach(b => {
2807
+ b.classList.remove('active', 'btn-secondary');
2808
+ b.classList.add('btn-outline-secondary');
2809
+ });
2810
+ e.target.classList.remove('btn-outline-secondary');
2811
+ e.target.classList.add('active', 'btn-secondary');
2812
+
2813
+ const letter = e.target.dataset.letter;
2814
+ document.querySelectorAll('#product-grid .product-card').forEach(card => {
2815
+ if (letter === 'all' || card.dataset.letter === letter) {
2816
+ card.style.display = 'block';
2817
+ } else {
2818
+ card.style.display = 'none';
2819
+ }
2820
+ });
2821
+
2822
+ document.getElementById('product-search-results').style.display = 'none';
2823
+ document.getElementById('product-grid').style.display = 'grid';
2824
+ document.getElementById('product-search').value = '';
2825
+ }
2826
+ });
2827
+
2828
  const productSearchInput = document.getElementById('product-search');
2829
+ const productGridEl = document.getElementById('product-grid');
2830
+ const alphabetFilterEl = document.getElementById('alphabet-filter');
2831
  const productSearchResultsEl = document.getElementById('product-search-results');
2832
 
2833
  productSearchInput.addEventListener('input', e => {
2834
  const term = e.target.value.toLowerCase().trim();
2835
 
2836
  if (term === '') {
2837
+ productGridEl.style.display = 'grid';
2838
+ alphabetFilterEl.style.display = 'flex';
2839
  productSearchResultsEl.style.display = 'none';
2840
+ const activeFilter = document.querySelector('.filter-btn.active');
2841
+ if (activeFilter) activeFilter.click();
 
 
2842
  return;
2843
  }
2844
 
2845
+ productGridEl.style.display = 'none';
2846
+ alphabetFilterEl.style.display = 'none';
2847
  productSearchResultsEl.style.display = 'grid';
2848
 
2849
  const filtered = allProducts.filter(p => p.name.toLowerCase().includes(term) || p.barcode.toLowerCase().includes(term));
 
4479
  <td class="text-end fw-bold">{{ format_currency_py(e.amount) }} ₸</td>
4480
  <td class="text-end">
4481
  <form action="{{ url_for('delete_personal_expense', expense_id=e.id) }}" method="POST" onsubmit="return confirm('Удалить этот расход?');">
4482
+ <button type="submit" class="btn btn-sm btn-outline-danger py-0 px-1"><i class="fas fa-trash">
4483
+ </i></button>
4484
  </form>
4485
  </td>
4486
  </tr>
 
4654
  {% endif %}
4655
  </td>
4656
  <td>
4657
+ {% if t.type == 'sale' and t.status in['completed', 'partially_returned'] %}
4658
  <a href="{{ url_for('return_transaction', transaction_id=t.id, cashier_id=user.id) }}" class="btn btn-sm btn-warning">Возврат</a>
4659
  {% endif %}
4660
  </td>
 
4759
  </div>
4760
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Сохранить</button></div>
4761
  </form>
4762
+ </div>
4763
  </div>
4764
  </div>
4765
  """