Kgshop commited on
Commit
d3e111a
·
verified ·
1 Parent(s): bfbea10

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -133
app.py CHANGED
@@ -89,7 +89,7 @@ def load_json_data(file_key):
89
  with open(filepath, 'r', encoding='utf-8') as f:
90
  return json.load(f)
91
  except (FileNotFoundError, json.JSONDecodeError):
92
- return []
93
 
94
  def save_json_data(file_key, data):
95
  filepath, lock = DATA_FILES[file_key]
@@ -232,22 +232,15 @@ def sales_screen():
232
  inventory = load_json_data('inventory')
233
  kassas = load_json_data('kassas')
234
 
235
- active_inventory = []
236
  for p in inventory:
237
- if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants', [])):
238
  active_inventory.append(p)
239
 
240
  active_inventory.sort(key=lambda x: x.get('name', '').lower())
241
 
242
- grouped_inventory = defaultdict(list)
243
- for p in active_inventory:
244
- first_letter = p.get('name', '#')[0].upper()
245
- grouped_inventory[first_letter].append(p)
246
-
247
- sorted_grouped_inventory = sorted(grouped_inventory.items())
248
-
249
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
250
- return render_template_string(html, inventory=active_inventory, kassas=kassas, grouped_inventory=sorted_grouped_inventory)
251
 
252
  @app.route('/inventory', methods=['GET', 'POST'])
253
  @admin_required
@@ -316,7 +309,7 @@ def inventory_management():
316
 
317
  for product in inventory_list:
318
  if isinstance(product, dict) and 'variants' in product:
319
- for variant in product.get('variants', []):
320
  stock = variant.get('stock', 0)
321
  cost_price = to_decimal(variant.get('cost_price', '0'))
322
  price = to_decimal(variant.get('price', '0'))
@@ -359,7 +352,7 @@ def edit_product(product_id):
359
  inventory[i]['name'] = name
360
  inventory[i]['barcode'] = barcode
361
 
362
- new_variants = []
363
  variant_ids = request.form.getlist('variant_id[]')
364
  variant_names = request.form.getlist('variant_name[]')
365
  variant_prices = request.form.getlist('variant_price[]')
@@ -402,7 +395,7 @@ def edit_product(product_id):
402
  def delete_product(product_id):
403
  inventory = load_json_data('inventory')
404
  initial_len = len(inventory)
405
- inventory = [p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
406
  if len(inventory) < initial_len:
407
  save_json_data('inventory', inventory)
408
  upload_db_to_hf('inventory')
@@ -433,7 +426,7 @@ def stock_in():
433
 
434
  variant_found = False
435
  variant_name_for_log = ""
436
- for i, variant in enumerate(product.get('variants', [])):
437
  if variant.get('id') == variant_id:
438
  variant_name_for_log = variant.get('option_value', '')
439
 
@@ -534,7 +527,7 @@ def complete_sale():
534
  if not user or not kassa:
535
  return jsonify({'success': False, 'message': 'Кассир или касса не найдены.'}), 404
536
 
537
- sale_items = []
538
  total_amount = Decimal('0.00')
539
  inventory_updates = {}
540
 
@@ -563,7 +556,7 @@ def complete_sale():
563
  if not product:
564
  return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
565
 
566
- variant = find_item_by_field(product.get('variants', []), 'id', variant_id)
567
  if not variant:
568
  return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
569
 
@@ -635,7 +628,7 @@ def complete_sale():
635
  current_balance = to_decimal(k.get('balance', '0'))
636
  kassas[i]['balance'] = str(current_balance + total_amount)
637
  if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
638
- kassas[i]['history'] = []
639
  kassas[i]['history'].append({
640
  'type': 'sale',
641
  'amount': str(total_amount),
@@ -686,13 +679,13 @@ def transaction_history():
686
  transactions = load_json_data('transactions')
687
  kassas = load_json_data('kassas')
688
 
689
- filtered_transactions = [
690
  t for t in transactions
691
  if datetime.fromisoformat(t['timestamp']).date() == selected_date
692
  ]
693
 
694
  if selected_kassa_id:
695
- filtered_transactions = [
696
  t for t in filtered_transactions
697
  if t.get('kassa_id') == selected_kassa_id
698
  ]
@@ -702,7 +695,7 @@ def transaction_history():
702
  total_quantity_sold = 0
703
  for t in filtered_transactions:
704
  if t.get('type') == 'sale':
705
- for item in t.get('items', []):
706
  total_quantity_sold += int(item.get('quantity', 0))
707
 
708
  filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
@@ -715,7 +708,7 @@ def transaction_history():
715
  def edit_transaction(transaction_id):
716
  try:
717
  data = request.get_json()
718
- items_update = data.get('items', [])
719
 
720
  transactions = load_json_data('transactions')
721
  kassas = load_json_data('kassas')
@@ -770,7 +763,7 @@ def edit_transaction(transaction_id):
770
  for i, k in enumerate(kassas):
771
  if k.get('id') == kassa_id:
772
  k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
773
- k.setdefault('history', []).append({
774
  'type': 'correction',
775
  'amount': str(amount_diff),
776
  'timestamp': get_current_time().isoformat(),
@@ -801,14 +794,14 @@ def delete_transaction(transaction_id):
801
  flash("Транзакция не найдена.", "danger")
802
  return redirect(url_for('transaction_history'))
803
 
804
- for item in transaction_to_delete.get('items', []):
805
  if item.get('is_custom'):
806
  continue
807
 
808
  product = find_item_by_field(inventory, 'id', item.get('product_id'))
809
  if not product: continue
810
 
811
- variant = find_item_by_field(product.get('variants', []), 'id', item.get('variant_id'))
812
  if not variant: continue
813
 
814
  quantity_change = item.get('quantity', 0)
@@ -826,14 +819,14 @@ def delete_transaction(transaction_id):
826
 
827
  kassa['balance'] = str(current_balance - amount_change)
828
 
829
- kassa.setdefault('history', []).append({
830
  'type': 'deletion',
831
  'amount': str(-amount_change),
832
  'timestamp': get_current_time().isoformat(),
833
  'description': f"Удаление транзакции {transaction_id[:8]}"
834
  })
835
 
836
- transactions = [t for t in transactions if t.get('id') != transaction_id]
837
 
838
  if transaction_to_delete.get('type') == 'return':
839
  original_id = transaction_to_delete.get('original_transaction_id')
@@ -870,15 +863,15 @@ def reports():
870
  personal_expenses = load_json_data('personal_expenses')
871
  users = load_json_data('users')
872
 
873
- filtered_transactions = [
874
  t for t in transactions
875
  if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
876
  ]
877
- filtered_expenses = [
878
  e for e in expenses
879
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
880
  ]
881
- filtered_personal_expenses = [
882
  e for e in personal_expenses
883
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
884
  ]
@@ -949,10 +942,10 @@ def product_roi_report():
949
  inventory = load_json_data('inventory')
950
  transactions = load_json_data('transactions')
951
 
952
- product_stats = []
953
 
954
  for product in inventory:
955
- for variant in product.get('variants', []):
956
  total_revenue = Decimal('0.00')
957
  total_cogs = Decimal('0.00')
958
  total_qty_sold = 0
@@ -1087,7 +1080,7 @@ def manage_kassa():
1087
  elif action == 'delete':
1088
  kassa_id = request.form.get('id')
1089
  initial_len = len(kassas)
1090
- kassas = [k for k in kassas if k.get('id') != kassa_id]
1091
  if len(kassas) < initial_len:
1092
  flash("Касса удалена.", "success")
1093
  else:
@@ -1125,7 +1118,7 @@ def kassa_operation():
1125
  new_balance -= amount
1126
 
1127
  kassas[i]['balance'] = str(new_balance)
1128
- if 'history' not in kassas[i]: kassas[i]['history'] = []
1129
  kassas[i]['history'].append({
1130
  'type': op_type,
1131
  'amount': str(amount),
@@ -1209,7 +1202,7 @@ def manage_personal_expense():
1209
  def delete_personal_expense(expense_id):
1210
  expenses = load_json_data('personal_expenses')
1211
  initial_len = len(expenses)
1212
- expenses = [e for e in expenses if e.get('id') != expense_id]
1213
  if len(expenses) < initial_len:
1214
  save_json_data('personal_expenses', expenses)
1215
  upload_db_to_hf('personal_expenses')
@@ -1301,7 +1294,7 @@ def end_shift():
1301
  kassa = find_item_by_field(kassas, 'id', shift['kassa_id'])
1302
  shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
1303
 
1304
- shift_transactions = [
1305
  t for t in transactions
1306
  if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
1307
  ]
@@ -1332,7 +1325,7 @@ def cashier_dashboard(user_id):
1332
  abort(404, "Кассир не найден")
1333
 
1334
  transactions = load_json_data('transactions')
1335
- user_transactions = [t for t in transactions if t.get('user_id') == user_id]
1336
  user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
1337
 
1338
  html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
@@ -1360,14 +1353,14 @@ def return_transaction(transaction_id):
1360
 
1361
  total_amount = to_decimal(original_transaction['total_amount'])
1362
 
1363
- return_items = []
1364
  inventory_updates = {}
1365
  for item in original_transaction['items']:
1366
  return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
1367
  if not item.get('is_custom'):
1368
  product = find_item_by_field(inventory, 'id', item['product_id'])
1369
  if product:
1370
- variant = find_item_by_field(product.get('variants', []), 'id', item['variant_id'])
1371
  if variant:
1372
  inventory_updates[item['variant_id']] = {'product_id': item['product_id'], 'new_stock': variant.get('stock', 0) + item['quantity']}
1373
 
@@ -1398,7 +1391,7 @@ def return_transaction(transaction_id):
1398
  for variant_id, update_info in inventory_updates.items():
1399
  for p in inventory:
1400
  if p.get('id') == update_info['product_id']:
1401
- for v in p.get('variants', []):
1402
  if v.get('id') == variant_id:
1403
  v['stock'] = update_info['new_stock']
1404
  p['timestamp_updated'] = now_iso
@@ -1410,7 +1403,7 @@ def return_transaction(transaction_id):
1410
  if k['id'] == original_transaction['kassa_id']:
1411
  current_balance = to_decimal(k.get('balance', '0'))
1412
  kassas[i]['balance'] = str(current_balance - total_amount)
1413
- kassas[i].setdefault('history', []).append({
1414
  'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
1415
  'transaction_id': return_transaction['id']
1416
  })
@@ -1440,7 +1433,7 @@ def backup_hf():
1440
  @app.route('/download', methods=['GET'])
1441
  @admin_required
1442
  def download_hf():
1443
- errors = []
1444
  success_count = 0
1445
  for key in DATA_FILES.keys():
1446
  filepath, _ = DATA_FILES[key]
@@ -1500,13 +1493,10 @@ BASE_TEMPLATE = """
1500
  .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
1501
  .sidebar.active { transform: translateX(0); }
1502
  .main-content { margin-left: 0; }
1503
- }
1504
- [data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
1505
- [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; }
1506
- [data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }
1507
- [data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
1508
- [data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }
1509
- [data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
1510
  [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
1511
  .product-card { cursor: pointer; }
1512
  .product-card:hover { border-color: var(--bs-primary); }
@@ -1525,7 +1515,7 @@ BASE_TEMPLATE = """
1525
  <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>
1526
  <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>
1527
  <li class="nav-item dropdown">
1528
- <a class="nav-link dropdown-toggle {% if request.endpoint in ['reports', 'product_roi_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
1529
  <i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
1530
  </a>
1531
  <ul class="dropdown-menu dropdown-menu-dark">
@@ -1533,8 +1523,8 @@ BASE_TEMPLATE = """
1533
  <li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
1534
  </ul>
1535
  </li>
1536
- <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>
1537
- <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>
1538
  {% if session.admin_logged_in %}
1539
  <li class="nav-item"><a class="nav-link" href="{{ url_for('admin_logout') }}"><i class="fas fa-fw fa-sign-out-alt me-2"></i>Выйти (Админ)</a></li>
1540
  {% endif %}
@@ -1651,36 +1641,7 @@ SALES_SCREEN_CONTENT = """
1651
  </div>
1652
  <input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
1653
 
1654
- <div id="product-accordion" class="accordion">
1655
- {% for letter, products in grouped_inventory %}
1656
- <div class="accordion-item">
1657
- <h2 class="accordion-header" id="heading-{{ letter }}">
1658
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ letter }}" aria-expanded="false" aria-controls="collapse-{{ letter }}">
1659
- {{ letter }}
1660
- </button>
1661
- </h2>
1662
- <div id="collapse-{{ letter }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ letter }}" data-bs-parent="#product-accordion">
1663
- <div class="accordion-body d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
1664
- {% for p in products %}
1665
- <div class="card text-center product-card" data-barcode="{{ p.barcode }}">
1666
- <div class="card-body p-2">
1667
- <h6 class="card-title small mb-1">{{ p.name }}</h6>
1668
- <p class="card-text fw-bold mb-0">
1669
- {% if p.variants|length > 1 %}
1670
- от {{ format_currency_py(p.variants|map(attribute='price')|min) }} с
1671
- {% elif p.variants|length == 1 %}
1672
- {{ format_currency_py(p.variants[0].price) }} с
1673
- {% else %}
1674
- Нет в наличии
1675
- {% endif %}
1676
- </p>
1677
- </div>
1678
- </div>
1679
- {% endfor %}
1680
- </div>
1681
- </div>
1682
- </div>
1683
- {% endfor %}
1684
  </div>
1685
  </div>
1686
  </div>
@@ -1764,12 +1725,14 @@ SALES_SCREEN_SCRIPTS = """
1764
  <script>
1765
  document.addEventListener('DOMContentLoaded', () => {
1766
  const cart = {};
1767
- const productGrid = document.getElementById('product-accordion');
1768
  const cartItemsEl = document.getElementById('cart-items');
1769
  const cartTotalEl = document.getElementById('cart-total');
1770
  let audioCtx;
1771
  let isScannerPaused = false;
1772
 
 
 
1773
  const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
1774
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
1775
  const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
@@ -1803,6 +1766,40 @@ document.addEventListener('DOMContentLoaded', () => {
1803
  return parseFloat(String(stringNumber).replace(/\\s/g, '').replace(',', '.')) || 0;
1804
  }
1805
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1806
  const updateCartView = () => {
1807
  cartItemsEl.innerHTML = '';
1808
  let total = 0;
@@ -1941,32 +1938,6 @@ document.addEventListener('DOMContentLoaded', () => {
1941
  updateCartView();
1942
  });
1943
 
1944
- document.getElementById('product-search').addEventListener('input', e => {
1945
- const term = e.target.value.toLowerCase();
1946
- const productCards = document.querySelectorAll('#product-accordion .product-card');
1947
- productCards.forEach(card => {
1948
- const productName = card.querySelector('.card-title').textContent.toLowerCase();
1949
- const barcode = card.dataset.barcode.toLowerCase();
1950
- const show = productName.includes(term) || barcode.includes(term);
1951
- card.style.display = show ? '' : 'none';
1952
- });
1953
- document.querySelectorAll('#product-accordion .accordion-item').forEach(accordionItem => {
1954
- const collapseElement = accordionItem.querySelector('.accordion-collapse');
1955
- const matchingCardsInGroup = accordionItem.querySelectorAll('.product-card:not([style*="display: none"])');
1956
- const bsCollapse = bootstrap.Collapse.getOrCreateInstance(collapseElement, { toggle: false });
1957
-
1958
- if (term === '') {
1959
- bsCollapse.hide();
1960
- } else {
1961
- if (matchingCardsInGroup.length > 0) {
1962
- bsCollapse.show();
1963
- } else {
1964
- bsCollapse.hide();
1965
- }
1966
- }
1967
- });
1968
- });
1969
-
1970
  const completeSale = (paymentMethod) => {
1971
  if (!session.shift || !session.cashier || !session.kassa) {
1972
  alert('Смена не активна. Начните смену, чтобы проводить продажи.');
@@ -2354,6 +2325,14 @@ INVENTORY_CONTENT = """
2354
  <div class="modal-header"><h5 class="modal-title">Оприходование товара</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
2355
  <form action="{{ url_for('stock_in') }}" method="POST">
2356
  <div class="modal-body">
 
 
 
 
 
 
 
 
2357
  <div class="mb-3">
2358
  <label for="stockin-product" class="form-label">Товар</label>
2359
  <select id="stockin-product" name="product_id" class="form-select" required>
@@ -2402,31 +2381,42 @@ document.addEventListener('DOMContentLoaded', () => {
2402
  document.querySelectorAll('.scan-modal-btn').forEach(btn => {
2403
  btn.addEventListener('click', e => {
2404
  const form = e.target.closest('form');
2405
- const scannerContainer = form.querySelector('[id^="modal-scanner-"]');
2406
- const barcodeInput = form.querySelector('.barcode-input');
2407
-
2408
- if (currentScanner) {
2409
- try { currentScanner.stop(); } catch(e) {}
2410
- currentScanner = null;
2411
- if(currentScannerContainer) currentScannerContainer.style.display = 'none';
2412
- return;
 
2413
  }
2414
- scannerContainer.style.display = 'block';
2415
- currentScannerContainer = scannerContainer;
2416
- const scannerId = scannerContainer.id + '-reader';
2417
- if(!document.getElementById(scannerId)) scannerContainer.innerHTML = `<div id="${scannerId}" style="width: 100%;"></div>`;
2418
-
2419
- const html5QrCode = new Html5Qrcode(scannerId);
2420
- currentScanner = html5QrCode;
2421
- const onScanSuccess = (decodedText, decodedResult) => {
2422
- barcodeInput.value = decodedText;
2423
- try { html5QrCode.stop(); } catch(e) {}
2424
- currentScanner = null;
2425
- scannerContainer.style.display = 'none';
2426
- };
2427
- html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } }, onScanSuccess);
2428
  });
2429
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2430
 
2431
  document.querySelectorAll('.modal').forEach(modal => {
2432
  modal.addEventListener('hidden.bs.modal', () => {
@@ -2452,7 +2442,7 @@ document.addEventListener('DOMContentLoaded', () => {
2452
  const formData = new FormData();
2453
  formData.append('image', file);
2454
 
2455
- preview.src = "data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="; // Spinner
2456
 
2457
  fetch("{{ url_for('upload_image') }}", {
2458
  method: 'POST',
@@ -2528,6 +2518,18 @@ document.addEventListener('DOMContentLoaded', () => {
2528
  const inventoryData = JSON.parse('{{ inventory|tojson|safe }}');
2529
  const productSelect = document.getElementById('stockin-product');
2530
  const variantSelect = document.getElementById('stockin-variant');
 
 
 
 
 
 
 
 
 
 
 
 
2531
 
2532
  productSelect.addEventListener('change', () => {
2533
  const productId = productSelect.value;
@@ -2692,7 +2694,7 @@ document.addEventListener('DOMContentLoaded', () => {
2692
 
2693
  document.getElementById('save-trans-btn').addEventListener('click', () => {
2694
  const form = document.getElementById('edit-trans-form');
2695
- const items_update = [];
2696
  form.querySelectorAll('tbody tr').forEach(row => {
2697
  items_update.push({
2698
  id: row.dataset.itemId,
 
89
  with open(filepath, 'r', encoding='utf-8') as f:
90
  return json.load(f)
91
  except (FileNotFoundError, json.JSONDecodeError):
92
+ return[]
93
 
94
  def save_json_data(file_key, data):
95
  filepath, lock = DATA_FILES[file_key]
 
232
  inventory = load_json_data('inventory')
233
  kassas = load_json_data('kassas')
234
 
235
+ active_inventory =[]
236
  for p in inventory:
237
+ if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',[])):
238
  active_inventory.append(p)
239
 
240
  active_inventory.sort(key=lambda x: x.get('name', '').lower())
241
 
 
 
 
 
 
 
 
242
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
243
+ return render_template_string(html, inventory=active_inventory, kassas=kassas)
244
 
245
  @app.route('/inventory', methods=['GET', 'POST'])
246
  @admin_required
 
309
 
310
  for product in inventory_list:
311
  if isinstance(product, dict) and 'variants' in product:
312
+ for variant in product.get('variants',[]):
313
  stock = variant.get('stock', 0)
314
  cost_price = to_decimal(variant.get('cost_price', '0'))
315
  price = to_decimal(variant.get('price', '0'))
 
352
  inventory[i]['name'] = name
353
  inventory[i]['barcode'] = barcode
354
 
355
+ new_variants =[]
356
  variant_ids = request.form.getlist('variant_id[]')
357
  variant_names = request.form.getlist('variant_name[]')
358
  variant_prices = request.form.getlist('variant_price[]')
 
395
  def delete_product(product_id):
396
  inventory = load_json_data('inventory')
397
  initial_len = len(inventory)
398
+ inventory =[p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
399
  if len(inventory) < initial_len:
400
  save_json_data('inventory', inventory)
401
  upload_db_to_hf('inventory')
 
426
 
427
  variant_found = False
428
  variant_name_for_log = ""
429
+ for i, variant in enumerate(product.get('variants',[])):
430
  if variant.get('id') == variant_id:
431
  variant_name_for_log = variant.get('option_value', '')
432
 
 
527
  if not user or not kassa:
528
  return jsonify({'success': False, 'message': 'Кассир или касса не найдены.'}), 404
529
 
530
+ sale_items =[]
531
  total_amount = Decimal('0.00')
532
  inventory_updates = {}
533
 
 
556
  if not product:
557
  return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
558
 
559
+ variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
560
  if not variant:
561
  return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
562
 
 
628
  current_balance = to_decimal(k.get('balance', '0'))
629
  kassas[i]['balance'] = str(current_balance + total_amount)
630
  if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
631
+ kassas[i]['history'] =[]
632
  kassas[i]['history'].append({
633
  'type': 'sale',
634
  'amount': str(total_amount),
 
679
  transactions = load_json_data('transactions')
680
  kassas = load_json_data('kassas')
681
 
682
+ filtered_transactions =[
683
  t for t in transactions
684
  if datetime.fromisoformat(t['timestamp']).date() == selected_date
685
  ]
686
 
687
  if selected_kassa_id:
688
+ filtered_transactions =[
689
  t for t in filtered_transactions
690
  if t.get('kassa_id') == selected_kassa_id
691
  ]
 
695
  total_quantity_sold = 0
696
  for t in filtered_transactions:
697
  if t.get('type') == 'sale':
698
+ for item in t.get('items',[]):
699
  total_quantity_sold += int(item.get('quantity', 0))
700
 
701
  filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
 
708
  def edit_transaction(transaction_id):
709
  try:
710
  data = request.get_json()
711
+ items_update = data.get('items',[])
712
 
713
  transactions = load_json_data('transactions')
714
  kassas = load_json_data('kassas')
 
763
  for i, k in enumerate(kassas):
764
  if k.get('id') == kassa_id:
765
  k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
766
+ k.setdefault('history',[]).append({
767
  'type': 'correction',
768
  'amount': str(amount_diff),
769
  'timestamp': get_current_time().isoformat(),
 
794
  flash("Транзакция не найдена.", "danger")
795
  return redirect(url_for('transaction_history'))
796
 
797
+ for item in transaction_to_delete.get('items',[]):
798
  if item.get('is_custom'):
799
  continue
800
 
801
  product = find_item_by_field(inventory, 'id', item.get('product_id'))
802
  if not product: continue
803
 
804
+ variant = find_item_by_field(product.get('variants',[]), 'id', item.get('variant_id'))
805
  if not variant: continue
806
 
807
  quantity_change = item.get('quantity', 0)
 
819
 
820
  kassa['balance'] = str(current_balance - amount_change)
821
 
822
+ kassa.setdefault('history',[]).append({
823
  'type': 'deletion',
824
  'amount': str(-amount_change),
825
  'timestamp': get_current_time().isoformat(),
826
  'description': f"Удаление транзакции {transaction_id[:8]}"
827
  })
828
 
829
+ transactions =[t for t in transactions if t.get('id') != transaction_id]
830
 
831
  if transaction_to_delete.get('type') == 'return':
832
  original_id = transaction_to_delete.get('original_transaction_id')
 
863
  personal_expenses = load_json_data('personal_expenses')
864
  users = load_json_data('users')
865
 
866
+ filtered_transactions =[
867
  t for t in transactions
868
  if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
869
  ]
870
+ filtered_expenses =[
871
  e for e in expenses
872
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
873
  ]
874
+ filtered_personal_expenses =[
875
  e for e in personal_expenses
876
  if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
877
  ]
 
942
  inventory = load_json_data('inventory')
943
  transactions = load_json_data('transactions')
944
 
945
+ product_stats =[]
946
 
947
  for product in inventory:
948
+ for variant in product.get('variants',[]):
949
  total_revenue = Decimal('0.00')
950
  total_cogs = Decimal('0.00')
951
  total_qty_sold = 0
 
1080
  elif action == 'delete':
1081
  kassa_id = request.form.get('id')
1082
  initial_len = len(kassas)
1083
+ kassas =[k for k in kassas if k.get('id') != kassa_id]
1084
  if len(kassas) < initial_len:
1085
  flash("Касса удалена.", "success")
1086
  else:
 
1118
  new_balance -= amount
1119
 
1120
  kassas[i]['balance'] = str(new_balance)
1121
+ if 'history' not in kassas[i]: kassas[i]['history'] =[]
1122
  kassas[i]['history'].append({
1123
  'type': op_type,
1124
  'amount': str(amount),
 
1202
  def delete_personal_expense(expense_id):
1203
  expenses = load_json_data('personal_expenses')
1204
  initial_len = len(expenses)
1205
+ expenses =[e for e in expenses if e.get('id') != expense_id]
1206
  if len(expenses) < initial_len:
1207
  save_json_data('personal_expenses', expenses)
1208
  upload_db_to_hf('personal_expenses')
 
1294
  kassa = find_item_by_field(kassas, 'id', shift['kassa_id'])
1295
  shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
1296
 
1297
+ shift_transactions =[
1298
  t for t in transactions
1299
  if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
1300
  ]
 
1325
  abort(404, "Кассир не найден")
1326
 
1327
  transactions = load_json_data('transactions')
1328
+ user_transactions =[t for t in transactions if t.get('user_id') == user_id]
1329
  user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
1330
 
1331
  html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
 
1353
 
1354
  total_amount = to_decimal(original_transaction['total_amount'])
1355
 
1356
+ return_items =[]
1357
  inventory_updates = {}
1358
  for item in original_transaction['items']:
1359
  return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
1360
  if not item.get('is_custom'):
1361
  product = find_item_by_field(inventory, 'id', item['product_id'])
1362
  if product:
1363
+ variant = find_item_by_field(product.get('variants',[]), 'id', item['variant_id'])
1364
  if variant:
1365
  inventory_updates[item['variant_id']] = {'product_id': item['product_id'], 'new_stock': variant.get('stock', 0) + item['quantity']}
1366
 
 
1391
  for variant_id, update_info in inventory_updates.items():
1392
  for p in inventory:
1393
  if p.get('id') == update_info['product_id']:
1394
+ for v in p.get('variants',[]):
1395
  if v.get('id') == variant_id:
1396
  v['stock'] = update_info['new_stock']
1397
  p['timestamp_updated'] = now_iso
 
1403
  if k['id'] == original_transaction['kassa_id']:
1404
  current_balance = to_decimal(k.get('balance', '0'))
1405
  kassas[i]['balance'] = str(current_balance - total_amount)
1406
+ kassas[i].setdefault('history',[]).append({
1407
  'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
1408
  'transaction_id': return_transaction['id']
1409
  })
 
1433
  @app.route('/download', methods=['GET'])
1434
  @admin_required
1435
  def download_hf():
1436
+ errors =[]
1437
  success_count = 0
1438
  for key in DATA_FILES.keys():
1439
  filepath, _ = DATA_FILES[key]
 
1493
  .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
1494
  .sidebar.active { transform: translateX(0); }
1495
  .main-content { margin-left: 0; }
1496
+ }[data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
1497
+ [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; }
1498
+ [data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
1499
+ [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); }
 
 
 
1500
  [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
1501
  .product-card { cursor: pointer; }
1502
  .product-card:hover { border-color: var(--bs-primary); }
 
1515
  <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>
1516
  <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>
1517
  <li class="nav-item dropdown">
1518
+ <a class="nav-link dropdown-toggle {% if request.endpoint in['reports', 'product_roi_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
1519
  <i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
1520
  </a>
1521
  <ul class="dropdown-menu dropdown-menu-dark">
 
1523
  <li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
1524
  </ul>
1525
  </li>
1526
+ <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>
1527
+ <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>
1528
  {% if session.admin_logged_in %}
1529
  <li class="nav-item"><a class="nav-link" href="{{ url_for('admin_logout') }}"><i class="fas fa-fw fa-sign-out-alt me-2"></i>Выйти (Админ)</a></li>
1530
  {% endif %}
 
1641
  </div>
1642
  <input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
1643
 
1644
+ <div id="product-grid" class="d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); max-height: 65vh; overflow-y: auto;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1645
  </div>
1646
  </div>
1647
  </div>
 
1725
  <script>
1726
  document.addEventListener('DOMContentLoaded', () => {
1727
  const cart = {};
1728
+ const productGrid = document.getElementById('product-grid');
1729
  const cartItemsEl = document.getElementById('cart-items');
1730
  const cartTotalEl = document.getElementById('cart-total');
1731
  let audioCtx;
1732
  let isScannerPaused = false;
1733
 
1734
+ const activeInventory = JSON.parse('{{ inventory|tojson|safe }}');
1735
+
1736
  const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
1737
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
1738
  const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
 
1766
  return parseFloat(String(stringNumber).replace(/\\s/g, '').replace(',', '.')) || 0;
1767
  }
1768
 
1769
+ const renderProducts = (products) => {
1770
+ const toRender = products.slice(0, 200);
1771
+ productGrid.innerHTML = toRender.map(p => {
1772
+ let priceStr = 'Нет в наличии';
1773
+ if(p.variants && p.variants.length > 1) {
1774
+ const minPrice = Math.min(...p.variants.map(v => parseLocaleNumber(v.price)));
1775
+ priceStr = 'от ' + minPrice.toLocaleString('ru-RU', {minimumFractionDigits: 2}) + ' с';
1776
+ } else if(p.variants && p.variants.length === 1) {
1777
+ priceStr = parseLocaleNumber(p.variants[0].price).toLocaleString('ru-RU', {minimumFractionDigits: 2}) + ' с';
1778
+ }
1779
+ return `<div class="card text-center product-card" data-barcode="${p.barcode}">
1780
+ <div class="card-body p-2">
1781
+ <h6 class="card-title small mb-1">${p.name}</h6>
1782
+ <p class="card-text fw-bold mb-0">${priceStr}</p>
1783
+ </div>
1784
+ </div>`;
1785
+ }).join('');
1786
+ };
1787
+
1788
+ renderProducts(activeInventory);
1789
+
1790
+ document.getElementById('product-search').addEventListener('input', e => {
1791
+ const term = e.target.value.toLowerCase().trim();
1792
+ if(!term) {
1793
+ renderProducts(activeInventory);
1794
+ return;
1795
+ }
1796
+ const filtered = activeInventory.filter(p =>
1797
+ (p.name && p.name.toLowerCase().includes(term)) ||
1798
+ (p.barcode && p.barcode.toLowerCase().includes(term))
1799
+ );
1800
+ renderProducts(filtered);
1801
+ });
1802
+
1803
  const updateCartView = () => {
1804
  cartItemsEl.innerHTML = '';
1805
  let total = 0;
 
1938
  updateCartView();
1939
  });
1940
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1941
  const completeSale = (paymentMethod) => {
1942
  if (!session.shift || !session.cashier || !session.kassa) {
1943
  alert('Смена не активна. Начните смену, чтобы проводить продажи.');
 
2325
  <div class="modal-header"><h5 class="modal-title">Оприходование товара</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
2326
  <form action="{{ url_for('stock_in') }}" method="POST">
2327
  <div class="modal-body">
2328
+ <div class="mb-3">
2329
+ <label class="form-label">Поиск по штрих-коду</label>
2330
+ <div class="input-group">
2331
+ <input type="text" id="stockin-barcode" class="form-control barcode-input" placeholder="Отсканируйте или введите">
2332
+ <button type="button" class="btn btn-outline-secondary scan-modal-btn"><i class="fas fa-barcode"></i></button>
2333
+ </div>
2334
+ </div>
2335
+ <div id="modal-scanner-stockin" class="mb-2" style="display:none;"></div>
2336
  <div class="mb-3">
2337
  <label for="stockin-product" class="form-label">Товар</label>
2338
  <select id="stockin-product" name="product_id" class="form-select" required>
 
2381
  document.querySelectorAll('.scan-modal-btn').forEach(btn => {
2382
  btn.addEventListener('click', e => {
2383
  const form = e.target.closest('form');
2384
+ if(!form) {
2385
+ const modal = e.target.closest('.modal-content');
2386
+ const scannerContainer = modal.querySelector('[id^="modal-scanner-"]');
2387
+ const barcodeInput = modal.querySelector('.barcode-input');
2388
+ toggleScanner(scannerContainer, barcodeInput);
2389
+ } else {
2390
+ const scannerContainer = form.querySelector('[id^="modal-scanner-"]');
2391
+ const barcodeInput = form.querySelector('.barcode-input');
2392
+ toggleScanner(scannerContainer, barcodeInput);
2393
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2394
  });
2395
  });
2396
+
2397
+ function toggleScanner(scannerContainer, barcodeInput) {
2398
+ if (currentScanner) {
2399
+ try { currentScanner.stop(); } catch(e) {}
2400
+ currentScanner = null;
2401
+ if(currentScannerContainer) currentScannerContainer.style.display = 'none';
2402
+ return;
2403
+ }
2404
+ scannerContainer.style.display = 'block';
2405
+ currentScannerContainer = scannerContainer;
2406
+ const scannerId = scannerContainer.id + '-reader';
2407
+ if(!document.getElementById(scannerId)) scannerContainer.innerHTML = `<div id="${scannerId}" style="width: 100%;"></div>`;
2408
+
2409
+ const html5QrCode = new Html5Qrcode(scannerId);
2410
+ currentScanner = html5QrCode;
2411
+ const onScanSuccess = (decodedText, decodedResult) => {
2412
+ barcodeInput.value = decodedText;
2413
+ barcodeInput.dispatchEvent(new Event('input'));
2414
+ try { html5QrCode.stop(); } catch(e) {}
2415
+ currentScanner = null;
2416
+ scannerContainer.style.display = 'none';
2417
+ };
2418
+ html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } }, onScanSuccess);
2419
+ }
2420
 
2421
  document.querySelectorAll('.modal').forEach(modal => {
2422
  modal.addEventListener('hidden.bs.modal', () => {
 
2442
  const formData = new FormData();
2443
  formData.append('image', file);
2444
 
2445
+ preview.src = "data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
2446
 
2447
  fetch("{{ url_for('upload_image') }}", {
2448
  method: 'POST',
 
2518
  const inventoryData = JSON.parse('{{ inventory|tojson|safe }}');
2519
  const productSelect = document.getElementById('stockin-product');
2520
  const variantSelect = document.getElementById('stockin-variant');
2521
+ const stockinBarcode = document.getElementById('stockin-barcode');
2522
+
2523
+ if (stockinBarcode) {
2524
+ stockinBarcode.addEventListener('input', (e) => {
2525
+ const term = e.target.value.trim().toLowerCase();
2526
+ const product = inventoryData.find(p => (p.barcode || '').toLowerCase() === term);
2527
+ if (product) {
2528
+ productSelect.value = product.id;
2529
+ productSelect.dispatchEvent(new Event('change'));
2530
+ }
2531
+ });
2532
+ }
2533
 
2534
  productSelect.addEventListener('change', () => {
2535
  const productId = productSelect.value;
 
2694
 
2695
  document.getElementById('save-trans-btn').addEventListener('click', () => {
2696
  const form = document.getElementById('edit-trans-form');
2697
+ const items_update =[];
2698
  form.querySelectorAll('tbody tr').forEach(row => {
2699
  items_update.push({
2700
  id: row.dataset.itemId,