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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -147
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]
@@ -142,7 +142,7 @@ def periodic_backup():
142
  for key in DATA_FILES.keys():
143
  upload_db_to_hf(key)
144
  except Exception as e:
145
- logging.error(f"Error scheduled backup: {e}", exc_info=True)
146
 
147
  def find_item_by_field(data, field, value):
148
  for item in data:
@@ -162,7 +162,7 @@ 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
 
@@ -187,9 +187,7 @@ def generate_receipt_html(transaction):
187
  total_colspan = 4
188
 
189
  items_html = ""
190
- total_qty = 0
191
  for i, item in enumerate(transaction['items']):
192
- total_qty += int(item.get('quantity', 0))
193
  discount_cell = f"""<td style="text-align: right;">{format_currency_py(item.get('discount_per_item', '0'))}</td>""" if has_discounts else ""
194
  items_html += f"""
195
  <tr>
@@ -204,13 +202,7 @@ def generate_receipt_html(transaction):
204
 
205
  total_amount_from_db = to_decimal(transaction['total_amount'])
206
 
207
- totals_html = f"""
208
- <tr>
209
- <td colspan="{total_colspan}" style="text-align: right;">Общее кол-во:</td>
210
- <td style="text-align: right;">{total_qty} шт.</td>
211
- </tr>
212
- """
213
-
214
  if delivery_cost > 0:
215
  subtotal = total_amount_from_db - delivery_cost
216
  totals_html += f"""
@@ -258,20 +250,20 @@ def generate_receipt_html(transaction):
258
  <title>Накладная {transaction['id'][:8]}</title>
259
  <style>
260
  body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 10px; background-color: #f4f4f4; color: #333; }}
261
- .invoice-box {{ max-width: 1000px; margin: auto; padding: 20px; border: 1px solid #eee; background: white; box-shadow: 0 0 10px rgba(0,0,0,0.15); overflow-x: auto; }}
262
  .header {{ text-align: center; margin-bottom: 20px; }}
263
  .header h1 {{ margin: 0; font-size: 22px; font-weight: 600; }}
264
  .header p {{ margin: 2px 0; font-size: 14px; }}
265
  .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
266
  table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
267
- table th, table td {{ padding: 8px; border-bottom: 1px solid #eee; white-space: nowrap; }}
268
- table th {{ background: #f2f2f2; font-weight: bold; border-bottom: 2px solid #ddd; }}
269
  table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
270
  .footer-info {{ font-size: 14px; margin-top: 20px; }}
271
  .print-hide {{ display: block; }}
272
  @media print {{
273
  body {{ margin: 0; padding: 0; background-color: #fff; }}
274
- .invoice-box {{ box-shadow: none; border: none; margin: 0; padding: 0; overflow-x: visible; }}
275
  .print-hide {{ display: none; }}
276
  }}
277
  @media screen and (max-width: 600px) {{
@@ -295,13 +287,13 @@ def generate_receipt_html(transaction):
295
  <h1>Товарная накладная № {transaction['id'][:8]}</h1>
296
  <p>от {datetime.fromisoformat(transaction['timestamp']).strftime('%d.%m.%Y %H:%M')}</p>
297
  </div>
298
- <table style="width: 100%;">
299
  <thead>
300
  <tr>{table_headers}</tr>
301
  </thead>
302
  <tbody>{items_html}</tbody>
303
  </table>
304
- <table style="margin-top: 20px; width: 100%;">
305
  {totals_html}
306
  </table>
307
  {note_html}
@@ -368,15 +360,22 @@ def sales_screen():
368
  transactions = load_json_data('transactions')
369
  edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
370
 
371
- active_inventory =[]
372
  for p in inventory:
373
- if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',[])):
374
  active_inventory.append(p)
375
 
376
  active_inventory.sort(key=lambda x: x.get('name', '').lower())
377
 
 
 
 
 
 
 
 
378
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
379
- return render_template_string(html, inventory=active_inventory, kassas=kassas, edit_tx=edit_tx)
380
 
381
  @app.route('/inventory', methods=['GET', 'POST'])
382
  def inventory_management():
@@ -396,7 +395,7 @@ def inventory_management():
396
  flash(f"Товар со штрих-кодом {barcode} уже существует.", "warning")
397
  return redirect(url_for('inventory_management'))
398
 
399
- variants =[]
400
  variant_names = request.form.getlist('variant_name[]')
401
  variant_prices_regular = request.form.getlist('variant_price_regular[]')
402
  variant_prices_min = request.form.getlist('variant_price_min[]')
@@ -462,7 +461,7 @@ def inventory_management():
462
 
463
  for product in inventory_list:
464
  if isinstance(product, dict) and 'variants' in product:
465
- for variant in product.get('variants',[]):
466
  stock = variant.get('stock', 0)
467
  cost_price = to_decimal(variant.get('cost_price', '0'))
468
  price = to_decimal(variant.get('price_regular', variant.get('price', '0')))
@@ -504,7 +503,7 @@ def update_prices():
504
 
505
  for p in inventory:
506
  updated = False
507
- for v in p.get('variants',[]):
508
  if v.get('id') in updates:
509
  v['price_regular'] = str(to_decimal(updates[v['id']][0]))
510
  v['price_min'] = str(to_decimal(updates[v['id']][1]))
@@ -542,7 +541,7 @@ def edit_product(product_id):
542
  inventory[i]['name'] = name
543
  inventory[i]['barcode'] = barcode
544
 
545
- new_variants =[]
546
  variant_ids = request.form.getlist('variant_id[]')
547
  variant_names = request.form.getlist('variant_name[]')
548
  variant_prices_regular = request.form.getlist('variant_price_regular[]')
@@ -589,7 +588,7 @@ def edit_product(product_id):
589
  def delete_product(product_id):
590
  inventory = load_json_data('inventory')
591
  initial_len = len(inventory)
592
- inventory =[p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
593
  if len(inventory) < initial_len:
594
  save_json_data('inventory', inventory)
595
  upload_db_to_hf('inventory')
@@ -621,7 +620,7 @@ def stock_in():
621
 
622
  variant_found = False
623
  variant_name_for_log = ""
624
- for i, variant in enumerate(product.get('variants',[])):
625
  if variant.get('id') == variant_id:
626
  variant_name_for_log = variant.get('option_value', '')
627
 
@@ -705,7 +704,7 @@ def get_product_by_barcode(barcode):
705
  inventory = load_json_data('inventory')
706
  product = find_item_by_field(inventory, 'barcode', barcode)
707
  if product:
708
- active_variants =[v for v in product.get('variants', []) if v.get('stock', 0) > 0]
709
  if active_variants:
710
  product_copy = product.copy()
711
  product_copy['variants'] = active_variants
@@ -735,11 +734,11 @@ def complete_sale():
735
  if not original_tx:
736
  return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
737
 
738
- for item in original_tx.get('items',[]):
739
  if not item.get('is_custom'):
740
  for p in inventory:
741
  if p.get('id') == item.get('product_id'):
742
- for v in p.get('variants',[]):
743
  if v.get('id') == item.get('variant_id'):
744
  v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
745
  break
@@ -751,7 +750,7 @@ def complete_sale():
751
  current_balance = to_decimal(k.get('balance', '0'))
752
  amount = to_decimal(original_tx.get('total_amount', '0'))
753
  k['balance'] = str(current_balance - amount)
754
- k.setdefault('history',[]).append({
755
  'type': 'correction_revert',
756
  'amount': str(-amount),
757
  'timestamp': get_current_time().isoformat(),
@@ -775,7 +774,7 @@ def complete_sale():
775
  if not user or not kassa:
776
  return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
777
 
778
- sale_items =[]
779
  items_total = Decimal('0')
780
  inventory_updates = {}
781
 
@@ -804,7 +803,7 @@ def complete_sale():
804
  if not product:
805
  return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
806
 
807
- variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
808
  if not variant:
809
  return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
810
 
@@ -878,7 +877,7 @@ def complete_sale():
878
  for variant_id, update_info in inventory_updates.items():
879
  for p in inventory:
880
  if p.get('id') == update_info['product_id']:
881
- for v in p.get('variants',[]):
882
  if v.get('id') == variant_id:
883
  v['stock'] = update_info['new_stock']
884
  p['timestamp_updated'] = now_iso
@@ -945,13 +944,13 @@ def transaction_history():
945
  transactions = load_json_data('transactions')
946
  kassas = load_json_data('kassas')
947
 
948
- filtered_transactions =[
949
  t for t in transactions
950
  if datetime.fromisoformat(t['timestamp']).date() == selected_date
951
  ]
952
 
953
  if selected_kassa_id:
954
- filtered_transactions =[
955
  t for t in filtered_transactions
956
  if t.get('kassa_id') == selected_kassa_id
957
  ]
@@ -961,7 +960,7 @@ def transaction_history():
961
  total_quantity_sold = 0
962
  for t in filtered_transactions:
963
  if t.get('type') == 'sale':
964
- for item in t.get('items',[]):
965
  total_quantity_sold += int(item.get('quantity', 0))
966
 
967
  filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
@@ -974,7 +973,7 @@ def transaction_history():
974
  def edit_transaction(transaction_id):
975
  try:
976
  data = request.get_json()
977
- items_update = data.get('items',[])
978
 
979
  transactions = load_json_data('transactions')
980
  kassas = load_json_data('kassas')
@@ -992,7 +991,7 @@ def edit_transaction(transaction_id):
992
  old_total_amount = to_decimal(original_transaction['total_amount'])
993
 
994
  new_items_total = Decimal('0')
995
- updated_items =[]
996
 
997
  for item in original_transaction['items']:
998
  item_id = item.get('variant_id') or item.get('product_id')
@@ -1014,7 +1013,7 @@ def edit_transaction(transaction_id):
1014
  transactions[transaction_index]['total_amount'] = str(new_total_amount)
1015
 
1016
  if 'edits' not in transactions[transaction_index]:
1017
- transactions[transaction_index]['edits'] =[]
1018
 
1019
  transactions[transaction_index]['edits'].append({
1020
  'timestamp': get_current_time().isoformat(),
@@ -1031,7 +1030,7 @@ def edit_transaction(transaction_id):
1031
  for i, k in enumerate(kassas):
1032
  if k.get('id') == kassa_id:
1033
  k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
1034
- k.setdefault('history',[]).append({
1035
  'type': 'correction',
1036
  'amount': str(amount_diff),
1037
  'timestamp': get_current_time().isoformat(),
@@ -1062,14 +1061,14 @@ def delete_transaction(transaction_id):
1062
  flash("Транзакция не найдена.", "danger")
1063
  return redirect(url_for('transaction_history'))
1064
 
1065
- for item in transaction_to_delete.get('items',[]):
1066
  if item.get('is_custom'):
1067
  continue
1068
 
1069
  product = find_item_by_field(inventory, 'id', item.get('product_id'))
1070
  if not product: continue
1071
 
1072
- variant = find_item_by_field(product.get('variants',[]), 'id', item.get('variant_id'))
1073
  if not variant: continue
1074
 
1075
  quantity_change = item.get('quantity', 0)
@@ -1087,7 +1086,7 @@ def delete_transaction(transaction_id):
1087
 
1088
  kassa['balance'] = str(current_balance - amount_change)
1089
 
1090
- kassa.setdefault('history',[]).append({
1091
  'type': 'deletion',
1092
  'amount': str(-amount_change),
1093
  'timestamp': get_current_time().isoformat(),
@@ -1131,20 +1130,20 @@ def reports():
1131
  personal_expenses = load_json_data('personal_expenses')
1132
  users = load_json_data('users')
1133
 
1134
- filtered_transactions =[
1135
  t for t in transactions
1136
  if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
1137
  ]
1138
- filtered_expenses =[
1139
  e for e in expenses
1140
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1141
  ]
1142
- filtered_personal_expenses =[
1143
  e for e in personal_expenses
1144
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
1145
  ]
1146
 
1147
- return_transactions =[t for t in filtered_transactions if t.get('type') == 'return']
1148
  total_returns_amount = sum(to_decimal(t['total_amount']) for t in return_transactions)
1149
  total_returns_count = len(return_transactions)
1150
 
@@ -1223,7 +1222,7 @@ def employee_report():
1223
  end_date = (datetime.strptime(end_date_str, '%Y-%m-%d') + timedelta(days=1)).replace(tzinfo=ALMATY_TZ)
1224
 
1225
  transactions = load_json_data('transactions')
1226
- employee_transactions =[]
1227
  totals = {'sales': Decimal(0), 'returns': Decimal(0), 'net': Decimal(0)}
1228
 
1229
  if user_id:
@@ -1255,17 +1254,17 @@ def item_movement_report():
1255
  product_id = request.args.get('product_id', '')
1256
  variant_id = request.args.get('variant_id', '')
1257
 
1258
- movements =[]
1259
  selected_product = None
1260
  selected_variant = None
1261
 
1262
  if product_id:
1263
  selected_product = find_item_by_field(inventory, 'id', product_id)
1264
  if selected_product and variant_id:
1265
- selected_variant = find_item_by_field(selected_product.get('variants',[]), 'id', variant_id)
1266
 
1267
  for t in transactions:
1268
- for item in t.get('items',[]):
1269
  if item.get('product_id') == product_id and (not variant_id or item.get('variant_id') == variant_id):
1270
  movements.append({
1271
  'timestamp': t['timestamp'],
@@ -1300,10 +1299,10 @@ def product_roi_report():
1300
  inventory = load_json_data('inventory')
1301
  transactions = load_json_data('transactions')
1302
 
1303
- product_stats =[]
1304
 
1305
  for product in inventory:
1306
- for variant in product.get('variants',[]):
1307
  total_revenue = Decimal('0')
1308
  total_cogs = Decimal('0')
1309
  total_qty_sold = 0
@@ -1398,7 +1397,7 @@ def manage_user():
1398
  elif action == 'delete':
1399
  user_id = request.form.get('id')
1400
  initial_len = len(users)
1401
- users =[u for u in users if u.get('id') != user_id]
1402
  if len(users) < initial_len:
1403
  flash("Кассир удален.", "success")
1404
  else:
@@ -1422,7 +1421,7 @@ def manage_kassa():
1422
  'id': uuid.uuid4().hex,
1423
  'name': name,
1424
  'balance': str(balance),
1425
- 'history':[]
1426
  }
1427
  if balance > 0:
1428
  new_kassa['history'].append({
@@ -1439,7 +1438,7 @@ def manage_kassa():
1439
  elif action == 'delete':
1440
  kassa_id = request.form.get('id')
1441
  initial_len = len(kassas)
1442
- kassas =[k for k in kassas if k.get('id') != kassa_id]
1443
  if len(kassas) < initial_len:
1444
  flash("Касса удалена.", "success")
1445
  else:
@@ -1524,7 +1523,7 @@ def manage_expense():
1524
  def delete_expense(expense_id):
1525
  expenses = load_json_data('expenses')
1526
  initial_len = len(expenses)
1527
- expenses =[e for e in expenses if e.get('id') != expense_id]
1528
  if len(expenses) < initial_len:
1529
  save_json_data('expenses', expenses)
1530
  upload_db_to_hf('expenses')
@@ -1561,7 +1560,7 @@ def manage_personal_expense():
1561
  def delete_personal_expense(expense_id):
1562
  expenses = load_json_data('personal_expenses')
1563
  initial_len = len(expenses)
1564
- expenses =[e for e in expenses if e.get('id') != expense_id]
1565
  if len(expenses) < initial_len:
1566
  save_json_data('personal_expenses', expenses)
1567
  upload_db_to_hf('personal_expenses')
@@ -1583,7 +1582,7 @@ def manage_link():
1583
  flash("Ссылка добавлена.", "success")
1584
  elif action == 'delete':
1585
  link_id = request.form.get('id')
1586
- links =[l for l in links if l.get('id') != link_id]
1587
  flash("Ссылка удалена.", "success")
1588
  save_json_data('links', links)
1589
  upload_db_to_hf('links')
@@ -1680,7 +1679,7 @@ def end_shift():
1680
 
1681
  shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
1682
 
1683
- shift_transactions =[
1684
  t for t in transactions
1685
  if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
1686
  ]
@@ -1711,7 +1710,7 @@ def cashier_dashboard(user_id):
1711
  abort(404, "Кассир не найден")
1712
 
1713
  transactions = load_json_data('transactions')
1714
- user_transactions =[t for t in transactions if t.get('user_id') == user_id]
1715
  user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
1716
 
1717
  html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
@@ -1741,7 +1740,7 @@ def return_transaction(transaction_id):
1741
  flash("Не указан ID кассира.", "danger")
1742
  return redirect(url_for('cashier_login'))
1743
 
1744
- returnable_items =[]
1745
  already_returned = original_transaction.get('return_info', {}).get('returned_items', {})
1746
 
1747
  for item in original_transaction['items']:
@@ -1762,7 +1761,7 @@ def return_transaction(transaction_id):
1762
  flash("Не удалось определить кассира.", "danger")
1763
  return redirect(url_for('cashier_login'))
1764
 
1765
- return_items =[]
1766
  total_return_amount = Decimal('0')
1767
  inventory_updates = {}
1768
  items_to_process = defaultdict(int)
@@ -1807,7 +1806,7 @@ def return_transaction(transaction_id):
1807
  if not item.get('is_custom'):
1808
  product = find_item_by_field(inventory, 'id', item['product_id'])
1809
  if product:
1810
- variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
1811
  if variant:
1812
  inventory_updates[variant_id] = {'product_id': item['product_id'], 'stock_change': qty_to_return}
1813
 
@@ -1854,7 +1853,7 @@ def return_transaction(transaction_id):
1854
  if k.get('id') == original_transaction['kassa_id']:
1855
  current_balance = to_decimal(k.get('balance', '0'))
1856
  kassas[k_idx]['balance'] = str(current_balance - total_return_amount)
1857
- kassas[k_idx].setdefault('history',[]).append({
1858
  'type': 'return', 'amount': str(-total_return_amount), 'timestamp': now_iso,
1859
  'transaction_id': return_transaction_id
1860
  })
@@ -1884,7 +1883,7 @@ def backup_hf():
1884
  @app.route('/download', methods=['GET'])
1885
  @admin_required
1886
  def download_hf():
1887
- errors =[]
1888
  success_count = 0
1889
  for key in DATA_FILES.keys():
1890
  filepath, _ = DATA_FILES[key]
@@ -1955,7 +1954,7 @@ def restore_bill(bill_id):
1955
  if not bill_to_restore:
1956
  return jsonify({'success': False, 'message': 'Bill not found'}), 404
1957
 
1958
- remaining_bills =[b for b in bills if b.get('id') != bill_id]
1959
  save_json_data('held_bills', remaining_bills)
1960
  upload_db_to_hf('held_bills')
1961
  return jsonify({'success': True, 'bill': bill_to_restore})
@@ -2002,7 +2001,7 @@ def manage_customer():
2002
  elif action == 'delete':
2003
  customer_id = request.form.get('id')
2004
  initial_len = len(customers)
2005
- customers =[c for c in customers if c.get('id') != customer_id]
2006
  if len(customers) < initial_len:
2007
  flash("Клиент удален.", "success")
2008
  else:
@@ -2018,7 +2017,7 @@ def search_customers():
2018
  if not query:
2019
  return jsonify([])
2020
  customers = load_json_data('customers')
2021
- matches =[
2022
  c for c in customers
2023
  if query in c.get('name', '').lower() or query in c.get('phone', '')
2024
  ]
@@ -2064,11 +2063,13 @@ BASE_TEMPLATE = """
2064
  .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
2065
  .sidebar.active { transform: translateX(0); }
2066
  .main-content { margin-left: 0; }
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%); }[data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
 
2072
  [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
2073
  .product-card { cursor: pointer; }
2074
  .product-card:hover { border-color: var(--bs-primary); }
@@ -2087,7 +2088,7 @@ BASE_TEMPLATE = """
2087
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'inventory_management' %}active{% endif %}" href="{{ url_for('inventory_management') }}"><i class="fas fa-fw fa-boxes me-2"></i>Склад</a></li>
2088
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'transaction_history' %}active{% endif %}" href="{{ url_for('transaction_history') }}"><i class="fas fa-fw fa-history me-2"></i>Транзакции</a></li>
2089
  <li class="nav-item dropdown">
2090
- <a class="nav-link dropdown-toggle {% if request.endpoint in['reports', 'product_roi_report', 'employee_report', 'item_movement_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
2091
  <i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
2092
  </a>
2093
  <ul class="dropdown-menu dropdown-menu-dark">
@@ -2099,7 +2100,7 @@ BASE_TEMPLATE = """
2099
  </li>
2100
  <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>
2101
  <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>
2102
- <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>
2103
 
2104
  {% if not session.admin_logged_in %}
2105
  <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>
@@ -2338,13 +2339,40 @@ SALES_SCREEN_CONTENT = """
2338
  <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>
2339
  <button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
2340
  </div>
2341
-
2342
- <input type="text" id="product-search" class="form-control mb-2" placeholder="Поиск по названию или штрих-коду...">
2343
- <div id="alphabet-filter" class="d-flex flex-wrap gap-1 justify-content-end mb-3"></div>
2344
-
2345
  <div id="product-search-results" class="d-grid gap-2" style="display: none; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
2346
 
2347
- <div id="product-grid" class="d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2348
  </div>
2349
  </div>
2350
  </div>
@@ -2364,7 +2392,6 @@ SALES_SCREEN_CONTENT = """
2364
  </div>
2365
  <div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
2366
  <div class="mb-3">
2367
- <div class="d-flex justify-content-between"><span>Общее кол-во:</span><span id="cart-total-qty">0 шт.</span></div>
2368
  <div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
2369
  <div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
2370
  <hr class="my-1">
@@ -2476,7 +2503,6 @@ document.addEventListener('DOMContentLoaded', () => {
2476
  const cartSubtotalEl = document.getElementById('cart-subtotal');
2477
  const cartDeliveryEl = document.getElementById('cart-delivery');
2478
  const cartTotalEl = document.getElementById('cart-total');
2479
- const cartTotalQtyEl = document.getElementById('cart-total-qty');
2480
 
2481
  let audioCtx;
2482
  let isScannerPaused = false;
@@ -2525,63 +2551,6 @@ document.addEventListener('DOMContentLoaded', () => {
2525
  });
2526
  }
2527
 
2528
- const alphabetFilterEl = document.getElementById('alphabet-filter');
2529
- const productGridEl = document.getElementById('product-grid');
2530
- const productSearchResultsEl = document.getElementById('product-search-results');
2531
- const productSearchInput = document.getElementById('product-search');
2532
-
2533
- const letters = new Set();
2534
- allProducts.forEach(p => {
2535
- if(p.name) letters.add(p.name.trim().charAt(0).toUpperCase());
2536
- });
2537
- const sortedLetters = Array.from(letters).sort();
2538
-
2539
- let filterHtml = `<div class="btn-group flex-wrap" role="group">
2540
- <button type="button" class="btn btn-outline-primary btn-sm alpha-filter active" data-letter="ALL">Все</button>`;
2541
- sortedLetters.forEach(l => {
2542
- filterHtml += `<button type="button" class="btn btn-outline-primary btn-sm alpha-filter" data-letter="${l}">${l}</button>`;
2543
- });
2544
- filterHtml += `</div>`;
2545
- alphabetFilterEl.innerHTML = filterHtml;
2546
-
2547
- const renderGrid = (letter) => {
2548
- const filtered = letter === 'ALL' ? allProducts : allProducts.filter(p => p.name.trim().toUpperCase().startsWith(letter));
2549
- productGridEl.innerHTML = filtered.map(p => {
2550
- let priceText = 'Нет в наличии';
2551
- if (p.variants && p.variants.length > 0) {
2552
- const activeVariants = editTx ? p.variants : p.variants.filter(v => v.stock > 0);
2553
- if (activeVariants.length > 0) {
2554
- if (activeVariants.length === 1) {
2555
- priceText = `${formatCurrencyJS(activeVariants[0].price_regular || activeVariants[0].price)} ₸`;
2556
- } else {
2557
- const prices = activeVariants.map(v => parseFloat(v.price_regular || v.price));
2558
- priceText = `от ${formatCurrencyJS(Math.min(...prices))} ₸`;
2559
- }
2560
- }
2561
- }
2562
- return `
2563
- <div class="card text-center product-card" data-barcode="${p.barcode}">
2564
- <div class="card-body p-2">
2565
- <h6 class="card-title small mb-1">${p.name}</h6>
2566
- <p class="card-text fw-bold mb-0">${priceText}</p>
2567
- </div>
2568
- </div>`;
2569
- }).join('');
2570
- };
2571
-
2572
- alphabetFilterEl.addEventListener('click', e => {
2573
- if(e.target.classList.contains('alpha-filter')) {
2574
- document.querySelectorAll('.alpha-filter').forEach(btn => btn.classList.remove('active'));
2575
- e.target.classList.add('active');
2576
- renderGrid(e.target.dataset.letter);
2577
- productSearchInput.value = '';
2578
- productGridEl.style.display = 'grid';
2579
- productSearchResultsEl.style.display = 'none';
2580
- }
2581
- });
2582
-
2583
- renderGrid('ALL');
2584
-
2585
  function playBeep() {
2586
  if (!audioCtx) {
2587
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
@@ -2616,14 +2585,12 @@ document.addEventListener('DOMContentLoaded', () => {
2616
  const updateCartView = () => {
2617
  cartItemsEl.innerHTML = '';
2618
  let subtotal = 0;
2619
- let totalQty = 0;
2620
  if (Object.keys(cart).length === 0) {
2621
  cartItemsEl.innerHTML = '<p class="text-center text-muted">Корзина пуста</p>';
2622
  }
2623
  for (const id in cart) {
2624
  const item = cart[id];
2625
  subtotal += (parseLocaleNumber(item.price) - parseLocaleNumber(item.discount)) * item.quantity;
2626
- totalQty += item.quantity;
2627
 
2628
  let nameField = '';
2629
  let priceField = '';
@@ -2667,7 +2634,6 @@ document.addEventListener('DOMContentLoaded', () => {
2667
  cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
2668
  cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
2669
  cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
2670
- cartTotalQtyEl.textContent = totalQty + ' шт.';
2671
  };
2672
 
2673
  const addToCart = (product, variant, price, priceType) => {
@@ -2693,7 +2659,7 @@ document.addEventListener('DOMContentLoaded', () => {
2693
  const container = document.getElementById('price-options-container');
2694
  container.innerHTML = '';
2695
 
2696
- const prices =[
2697
  { type: 'Общая', value: variant.price_regular || variant.price },
2698
  { type: 'Минимальная', value: variant.price_min },
2699
  { type: 'Оптовая', value: variant.price_wholesale }
@@ -2833,17 +2799,24 @@ document.addEventListener('DOMContentLoaded', () => {
2833
  });
2834
  }
2835
 
 
 
 
 
2836
  productSearchInput.addEventListener('input', e => {
2837
  const term = e.target.value.toLowerCase().trim();
2838
 
2839
  if (term === '') {
2840
- productGridEl.style.display = 'grid';
2841
  productSearchResultsEl.style.display = 'none';
2842
  productSearchResultsEl.innerHTML = '';
 
 
 
2843
  return;
2844
  }
2845
 
2846
- productGridEl.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));
@@ -4653,7 +4626,7 @@ CASHIER_DASHBOARD_CONTENT = """
4653
  {% endif %}
4654
  </td>
4655
  <td>
4656
- {% if t.type == 'sale' and t.status in['completed', 'partially_returned'] %}
4657
  <a href="{{ url_for('return_transaction', transaction_id=t.id, cashier_id=user.id) }}" class="btn btn-sm btn-warning">Возврат</a>
4658
  {% endif %}
4659
  </td>
@@ -4768,4 +4741,4 @@ if __name__ == '__main__':
4768
  backup_thread.start()
4769
  for key in DATA_FILES.keys():
4770
  load_json_data(key)
4771
- app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)
 
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]
 
142
  for key in DATA_FILES.keys():
143
  upload_db_to_hf(key)
144
  except Exception as e:
145
+ logging.error(f"Error during scheduled backup: {e}", exc_info=True)
146
 
147
  def find_item_by_field(data, field, value):
148
  for item in data:
 
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
 
 
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>
 
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"""
 
250
  <title>Накладная {transaction['id'][:8]}</title>
251
  <style>
252
  body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 10px; background-color: #f4f4f4; color: #333; }}
253
+ .invoice-box {{ max-width: 800px; margin: auto; padding: 20px; border: 1px solid #eee; background: white; box-shadow: 0 0 10px rgba(0,0,0,0.15); }}
254
  .header {{ text-align: center; margin-bottom: 20px; }}
255
  .header h1 {{ margin: 0; font-size: 22px; font-weight: 600; }}
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; }}
264
  @media print {{
265
  body {{ margin: 0; padding: 0; background-color: #fff; }}
266
+ .invoice-box {{ box-shadow: none; border: none; margin: 0; padding: 0; }}
267
  .print-hide {{ display: none; }}
268
  }}
269
  @media screen and (max-width: 600px) {{
 
287
  <h1>Товарная накладная № {transaction['id'][:8]}</h1>
288
  <p>от {datetime.fromisoformat(transaction['timestamp']).strftime('%d.%m.%Y %H:%M')}</p>
289
  </div>
290
+ <table>
291
  <thead>
292
  <tr>{table_headers}</tr>
293
  </thead>
294
  <tbody>{items_html}</tbody>
295
  </table>
296
+ <table style="margin-top: 20px;">
297
  {totals_html}
298
  </table>
299
  {note_html}
 
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():
 
395
  flash(f"Товар со штрих-кодом {barcode} уже существует.", "warning")
396
  return redirect(url_for('inventory_management'))
397
 
398
+ variants = []
399
  variant_names = request.form.getlist('variant_name[]')
400
  variant_prices_regular = request.form.getlist('variant_price_regular[]')
401
  variant_prices_min = request.form.getlist('variant_price_min[]')
 
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')))
 
503
 
504
  for p in inventory:
505
  updated = False
506
+ for v in p.get('variants', []):
507
  if v.get('id') in updates:
508
  v['price_regular'] = str(to_decimal(updates[v['id']][0]))
509
  v['price_min'] = str(to_decimal(updates[v['id']][1]))
 
541
  inventory[i]['name'] = name
542
  inventory[i]['barcode'] = barcode
543
 
544
+ new_variants = []
545
  variant_ids = request.form.getlist('variant_id[]')
546
  variant_names = request.form.getlist('variant_name[]')
547
  variant_prices_regular = request.form.getlist('variant_price_regular[]')
 
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
 
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
  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
  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
  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
  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
  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
 
 
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
 
944
  transactions = load_json_data('transactions')
945
  kassas = load_json_data('kassas')
946
 
947
+ filtered_transactions = [
948
  t for t in transactions
949
  if datetime.fromisoformat(t['timestamp']).date() == selected_date
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
  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
  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')
 
991
  old_total_amount = to_decimal(original_transaction['total_amount'])
992
 
993
  new_items_total = Decimal('0')
994
+ updated_items = []
995
 
996
  for item in original_transaction['items']:
997
  item_id = item.get('variant_id') or item.get('product_id')
 
1013
  transactions[transaction_index]['total_amount'] = str(new_total_amount)
1014
 
1015
  if 'edits' not in transactions[transaction_index]:
1016
+ transactions[transaction_index]['edits'] = []
1017
 
1018
  transactions[transaction_index]['edits'].append({
1019
  'timestamp': get_current_time().isoformat(),
 
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
  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
 
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(),
 
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
  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
  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'],
 
1299
  inventory = load_json_data('inventory')
1300
  transactions = load_json_data('transactions')
1301
 
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
 
1397
  elif action == 'delete':
1398
  user_id = request.form.get('id')
1399
  initial_len = len(users)
1400
+ users = [u for u in users if u.get('id') != user_id]
1401
  if len(users) < initial_len:
1402
  flash("Кассир удален.", "success")
1403
  else:
 
1421
  'id': uuid.uuid4().hex,
1422
  'name': name,
1423
  'balance': str(balance),
1424
+ 'history': []
1425
  }
1426
  if balance > 0:
1427
  new_kassa['history'].append({
 
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:
 
1523
  def delete_expense(expense_id):
1524
  expenses = load_json_data('expenses')
1525
  initial_len = len(expenses)
1526
+ expenses = [e for e in expenses if e.get('id') != expense_id]
1527
  if len(expenses) < initial_len:
1528
  save_json_data('expenses', expenses)
1529
  upload_db_to_hf('expenses')
 
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
  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
 
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
  ]
 
1710
  abort(404, "Кассир не найден")
1711
 
1712
  transactions = load_json_data('transactions')
1713
+ user_transactions = [t for t in transactions if t.get('user_id') == user_id]
1714
  user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
1715
 
1716
  html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
 
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
  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
  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
 
 
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
  @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]
 
1954
  if not bill_to_restore:
1955
  return jsonify({'success': False, 'message': 'Bill not found'}), 404
1956
 
1957
+ remaining_bills = [b for b in bills if b.get('id') != bill_id]
1958
  save_json_data('held_bills', remaining_bills)
1959
  upload_db_to_hf('held_bills')
1960
  return jsonify({'success': True, 'bill': bill_to_restore})
 
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
  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
  .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); }
 
2088
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'inventory_management' %}active{% endif %}" href="{{ url_for('inventory_management') }}"><i class="fas fa-fw fa-boxes me-2"></i>Склад</a></li>
2089
  <li class="nav-item"><a class="nav-link {% if request.endpoint == 'transaction_history' %}active{% endif %}" href="{{ url_for('transaction_history') }}"><i class="fas fa-fw fa-history me-2"></i>Транзакции</a></li>
2090
  <li class="nav-item dropdown">
2091
+ <a class="nav-link dropdown-toggle {% if request.endpoint in ['reports', 'product_roi_report', 'employee_report', 'item_movement_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
2092
  <i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
2093
  </a>
2094
  <ul class="dropdown-menu dropdown-menu-dark">
 
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
+ <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 %}
2375
+ </div>
2376
  </div>
2377
  </div>
2378
  </div>
 
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
  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;
 
2551
  });
2552
  }
2553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2554
  function playBeep() {
2555
  if (!audioCtx) {
2556
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
 
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
  cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
2635
  cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
2636
  cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
 
2637
  };
2638
 
2639
  const addToCart = (product, variant, price, priceType) => {
 
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
  });
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));
 
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>
 
4741
  backup_thread.start()
4742
  for key in DATA_FILES.keys():
4743
  load_json_data(key)
4744
+ app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)