Kgshop commited on
Commit
7488049
·
verified ·
1 Parent(s): 5bc75f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -233
app.py CHANGED
@@ -36,6 +36,7 @@ SHIFTS_FILE = os.path.join(DATA_DIR, 'shifts.json')
36
  HELD_BILLS_FILE = os.path.join(DATA_DIR, 'held_bills.json')
37
  CUSTOMERS_FILE = os.path.join(DATA_DIR, 'customers.json')
38
  STOCK_HISTORY_FILE = os.path.join(DATA_DIR, 'stock_history.json')
 
39
 
40
  DATA_FILES = {
41
  'inventory': (INVENTORY_FILE, threading.Lock()),
@@ -48,6 +49,7 @@ DATA_FILES = {
48
  'held_bills': (HELD_BILLS_FILE, threading.Lock()),
49
  'customers': (CUSTOMERS_FILE, threading.Lock()),
50
  'stock_history': (STOCK_HISTORY_FILE, threading.Lock()),
 
51
  }
52
 
53
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE", "YOUR_WRITE_TOKEN_HERE")
@@ -67,11 +69,11 @@ class DecimalEncoder(json.JSONEncoder):
67
  return str(obj)
68
  return json.JSONEncoder.default(self, obj)
69
 
70
- def to_decimal(value_str, default='0.00'):
71
- if value_str is None or value_str == '':
72
  return Decimal(default)
73
  try:
74
- return Decimal(str(value_str).replace(',', '.'))
75
  except InvalidOperation:
76
  logging.warning(f"Could not convert '{value_str}' to Decimal. Returned {default}.")
77
  return Decimal(default)
@@ -158,9 +160,9 @@ def find_user_by_pin(pin):
158
  def format_currency_py(value):
159
  try:
160
  number = to_decimal(value)
161
- return f"{number:,.2f}".replace(",", " ").replace(".", ",")
162
  except (InvalidOperation, TypeError, ValueError):
163
- return "0,00"
164
 
165
  def generate_receipt_html(transaction):
166
  has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items', []))
@@ -233,6 +235,14 @@ def generate_receipt_html(transaction):
233
  <p style="margin: 0; white-space: pre-wrap; font-size: 14px;">{safe_note}</p>
234
  </div>
235
  """
 
 
 
 
 
 
 
 
236
 
237
  return f"""
238
  <!DOCTYPE html>
@@ -273,7 +283,8 @@ def generate_receipt_html(transaction):
273
  <body>
274
  <div class="invoice-box">
275
  <div class="print-hide" style="text-align: right; margin-bottom: 20px;">
276
- <button onclick="window.print()" style="padding: 8px 12px; font-size: 14px; cursor: pointer;">Печать</button>
 
277
  </div>
278
  <div class="header">
279
  <h1>Товарная накладная № {transaction['id'][:8]}</h1>
@@ -293,7 +304,27 @@ def generate_receipt_html(transaction):
293
  <p>Способ оплаты: {'Наличные' if transaction['payment_method'] == 'cash' else 'Карта'}</p>
294
  <p>Кассир: {transaction['user_name']}</p>
295
  </div>
 
296
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  </body>
298
  </html>
299
  """
@@ -311,11 +342,27 @@ def admin_required(f):
311
  def inject_utils():
312
  return {'format_currency_py': format_currency_py, 'get_current_time': get_current_time, 'quote': quote}
313
 
 
 
 
 
 
 
 
 
 
 
314
  @app.route('/')
315
  def sales_screen():
316
  inventory = load_json_data('inventory')
317
  kassas = load_json_data('kassas')
318
 
 
 
 
 
 
 
319
  active_inventory = []
320
  for p in inventory:
321
  if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants', [])):
@@ -331,7 +378,7 @@ def sales_screen():
331
  sorted_grouped_inventory = sorted(grouped_inventory.items())
332
 
333
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
334
- return render_template_string(html, inventory=active_inventory, kassas=kassas, grouped_inventory=sorted_grouped_inventory)
335
 
336
  @app.route('/inventory', methods=['GET', 'POST'])
337
  def inventory_management():
@@ -365,15 +412,15 @@ def inventory_management():
365
  if not v_name: continue
366
 
367
  if is_admin:
368
- v_price_regular = str(to_decimal(variant_prices_regular[i])) if i < len(variant_prices_regular) else '0.00'
369
- v_price_min = str(to_decimal(variant_prices_min[i])) if i < len(variant_prices_min) else '0.00'
370
- v_price_wholesale = str(to_decimal(variant_prices_wholesale[i])) if i < len(variant_prices_wholesale) else '0.00'
371
- v_cost = str(to_decimal(variant_cost_prices[i])) if i < len(variant_cost_prices) else '0.00'
372
  else:
373
- v_price_regular = '0.00'
374
- v_price_min = '0.00'
375
- v_price_wholesale = '0.00'
376
- v_cost = '0.00'
377
 
378
  variants.append({
379
  'id': uuid.uuid4().hex,
@@ -412,8 +459,8 @@ def inventory_management():
412
  inventory_list.sort(key=lambda x: x.get('name', '').lower())
413
 
414
  total_units = 0
415
- total_cost_value = Decimal('0.00')
416
- total_retail_value = Decimal('0.00')
417
 
418
  for product in inventory_list:
419
  if isinstance(product, dict) and 'variants' in product:
@@ -429,6 +476,7 @@ def inventory_management():
429
  potential_profit = total_retail_value - total_cost_value
430
 
431
  inventory_summary = {
 
432
  'total_units': total_units,
433
  'total_cost_value': total_cost_value,
434
  'potential_profit': potential_profit
@@ -587,7 +635,7 @@ def stock_in():
587
 
588
  if old_stock + quantity > 0:
589
  avg_cost = ((old_cost * old_stock) + (new_cost * quantity) + delivery_cost) / (old_stock + quantity)
590
- variant['cost_price'] = str(avg_cost.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
591
 
592
  variant_found = True
593
  break
@@ -673,20 +721,56 @@ def complete_sale():
673
  try:
674
  data = request.get_json()
675
  cart = data.get('cart', {})
676
- user_id = data.get('userId')
677
- kassa_id = data.get('kassaId')
678
- shift_id = data.get('shiftId')
679
  payment_method = data.get('paymentMethod', 'cash')
680
  delivery_cost_str = data.get('deliveryCost', '0')
681
  note = data.get('note', '')
682
-
683
- if not cart or not user_id or not kassa_id or not shift_id:
684
- return jsonify({'success': False, 'message': 'Неполные данные для продажи. Начните смену.'}), 400
685
 
686
  inventory = load_json_data('inventory')
687
  users = load_json_data('users')
688
  kassas = load_json_data('kassas')
689
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  user = find_item_by_field(users, 'id', user_id)
691
  kassa = find_item_by_field(kassas, 'id', kassa_id)
692
 
@@ -694,7 +778,7 @@ def complete_sale():
694
  return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
695
 
696
  sale_items = []
697
- items_total = Decimal('0.00')
698
  inventory_updates = {}
699
 
700
  for item_id, cart_item in cart.items():
@@ -710,8 +794,8 @@ def complete_sale():
710
  'barcode': 'CUSTOM',
711
  'quantity': quantity_sold,
712
  'price_at_sale': str(price_at_sale),
713
- 'cost_price_at_sale': '0.00',
714
- 'discount_per_item': '0.00',
715
  'total': str(item_total),
716
  'is_custom': True
717
  })
@@ -743,10 +827,12 @@ def complete_sale():
743
  item_total = final_price * Decimal(quantity_sold)
744
  items_total += item_total
745
 
 
 
746
  sale_items.append({
747
  'product_id': product['id'],
748
  'variant_id': variant_id,
749
- 'name': f"{product['name']} ({variant['option_value']})",
750
  'barcode': product.get('barcode'),
751
  'quantity': quantity_sold,
752
  'price_at_sale': str(price_at_sale),
@@ -761,8 +847,8 @@ def complete_sale():
761
  now_iso = get_current_time().isoformat()
762
 
763
  new_transaction = {
764
- 'id': uuid.uuid4().hex,
765
- 'timestamp': now_iso,
766
  'type': 'sale',
767
  'status': 'completed',
768
  'original_transaction_id': None,
@@ -778,9 +864,18 @@ def complete_sale():
778
  'payment_method': payment_method
779
  }
780
 
 
 
 
781
  new_transaction['invoice_html'] = generate_receipt_html(new_transaction)
782
- transactions = load_json_data('transactions')
783
- transactions.append(new_transaction)
 
 
 
 
 
 
784
 
785
  for variant_id, update_info in inventory_updates.items():
786
  for p in inventory:
@@ -800,10 +895,11 @@ def complete_sale():
800
  if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
801
  kassas[i]['history'] = []
802
  kassas[i]['history'].append({
803
- 'type': 'sale',
804
  'amount': str(total_amount),
805
  'timestamp': now_iso,
806
- 'transaction_id': new_transaction['id']
 
807
  })
808
  break
809
 
@@ -818,7 +914,7 @@ def complete_sale():
818
  receipt_url = url_for('view_receipt', transaction_id=new_transaction['id'], _external=True)
819
  return jsonify({
820
  'success': True,
821
- 'message': 'Продажа успешно зарегистрирована.',
822
  'transactionId': new_transaction['id'],
823
  'receiptUrl': receipt_url
824
  })
@@ -897,7 +993,7 @@ def edit_transaction(transaction_id):
897
  original_transaction = transactions[transaction_index]
898
  old_total_amount = to_decimal(original_transaction['total_amount'])
899
 
900
- new_items_total = Decimal('0.00')
901
  updated_items = []
902
 
903
  for item in original_transaction['items']:
@@ -1189,7 +1285,7 @@ def item_movement_report():
1189
  'timestamp': sh['timestamp'],
1190
  'type': 'stock_in',
1191
  'qty': sh['quantity'],
1192
- 'price': sh.get('cost_price', '0.00'),
1193
  'doc_id': 'Приход',
1194
  'user': 'Админ',
1195
  'variant_name': f"{sh.get('product_name')} ({sh.get('variant_name')})"
@@ -1210,8 +1306,8 @@ def product_roi_report():
1210
 
1211
  for product in inventory:
1212
  for variant in product.get('variants', []):
1213
- total_revenue = Decimal('0.00')
1214
- total_cogs = Decimal('0.00')
1215
  total_qty_sold = 0
1216
 
1217
  for t in transactions:
@@ -1254,10 +1350,11 @@ def admin_panel():
1254
  kassas = load_json_data('kassas')
1255
  expenses = load_json_data('expenses')
1256
  personal_expenses = load_json_data('personal_expenses')
 
1257
  expenses.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
1258
  personal_expenses.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
1259
  html = BASE_TEMPLATE.replace('__TITLE__', "Админ-панель").replace('__CONTENT__', ADMIN_CONTENT).replace('__SCRIPTS__', ADMIN_SCRIPTS)
1260
- return render_template_string(html, users=users, kassas=kassas, expenses=expenses, personal_expenses=personal_expenses)
1261
 
1262
  @app.route('/admin/shifts')
1263
  @admin_required
@@ -1475,6 +1572,25 @@ def delete_personal_expense(expense_id):
1475
  flash("Личный расход не найден.", "warning")
1476
  return redirect(url_for('admin_panel'))
1477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1478
  @app.route('/cashier_login', methods=['GET', 'POST'])
1479
  def cashier_login():
1480
  if request.method == 'POST':
@@ -1649,7 +1765,7 @@ def return_transaction(transaction_id):
1649
  return redirect(url_for('cashier_login'))
1650
 
1651
  return_items = []
1652
- total_return_amount = Decimal('0.00')
1653
  inventory_updates = {}
1654
  items_to_process = defaultdict(int)
1655
 
@@ -2030,11 +2146,11 @@ BASE_TEMPLATE = """
2030
  <div class="modal-dialog">
2031
  <div class="modal-content">
2032
  <div class="modal-header">
2033
- <h5 class="modal-title">Продажа завершена</h5>
2034
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
2035
  </div>
2036
  <div class="modal-body">
2037
- <p>Накладная успешно создана.</p>
2038
  <div class="mb-3">
2039
  <label for="whatsapp-phone" class="form-label">Отправить накладную на WhatsApp</label>
2040
  <div class="input-group">
@@ -2273,18 +2389,22 @@ SALES_SCREEN_CONTENT = """
2273
  <div id="session-info" class="small text-muted mt-1"></div>
2274
  </div>
2275
  <div class="card-body">
 
 
 
 
2276
  <div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
2277
  <div class="mb-3">
2278
- <div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0,00 ₸</span></div>
2279
- <div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0,00 ₸</span></div>
2280
  <hr class="my-1">
2281
- <div class="d-flex justify-content-between align-items-center h4"><span>Итого:</span><span id="cart-total">0,00 ₸</span></div>
2282
  </div>
2283
  <div class="btn-group w-100 mb-2">
2284
  <button class="btn btn-outline-secondary btn-sm" id="add-delivery-btn"><i class="fas fa-truck"></i> Доставка</button>
2285
  <button class="btn btn-outline-secondary btn-sm" id="add-note-btn"><i class="fas fa-sticky-note"></i> Заметка</button>
2286
  </div>
2287
- <div class="d-grid gap-2">
2288
  <div class="btn-group"><button class="btn btn-success flex-grow-1" id="pay-cash-btn"><i class="fas fa-money-bill-wave me-2"></i>Наличные</button><button class="btn btn-info flex-grow-1" id="pay-card-btn"><i class="far fa-credit-card me-2"></i>Карта</button></div>
2289
  <div class="btn-group">
2290
  <button class="btn btn-secondary" id="hold-bill-btn"><i class="fas fa-pause me-2"></i>Отложить накладную</button>
@@ -2338,8 +2458,8 @@ SALES_SCREEN_CONTENT = """
2338
  <form id="custom-item-form">
2339
  <div class="modal-body">
2340
  <div class="mb-3"><label class="form-label">Название (необязательно)</label><input type="text" id="custom-item-name" class="form-control" placeholder="Напр. 'Пакет'"></div>
2341
- <div class="mb-3"><label class="form-label">Цена за 1 шт.</label><input type="text" id="custom-item-price" class="form-control" inputmode="decimal" required></div>
2342
- <div class="mb-3"><label class="form-label">Количество</label><input type="number" id="custom-item-qty" class="form-control" value="1" min="1" required></div>
2343
  </div>
2344
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Добавить в накладную</button></div>
2345
  </form>
@@ -2405,6 +2525,35 @@ document.addEventListener('DOMContentLoaded', () => {
2405
  shift: null
2406
  };
2407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2408
  function playBeep() {
2409
  if (!audioCtx) {
2410
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
@@ -2429,10 +2578,10 @@ document.addEventListener('DOMContentLoaded', () => {
2429
  const formatCurrencyJS = (value) => {
2430
  try {
2431
  const number = parseFloat(String(value).replace(/\\s/g, '').replace(',', '.'));
2432
- if (isNaN(number)) return '0,00';
2433
- return number.toLocaleString('ru-RU', {minimumFractionDigits: 2, maximumFractionDigits: 2});
2434
  } catch (e) {
2435
- return '0,00';
2436
  }
2437
  };
2438
 
@@ -2449,8 +2598,8 @@ document.addEventListener('DOMContentLoaded', () => {
2449
  <div class="list-group-item">
2450
  <div class="d-flex justify-content-between align-items-start">
2451
  <div>
2452
- <h6 class="mb-0 small">${item.productName} ${item.variantName ? '('+item.variantName+')' : ''}</h6>
2453
- <small>${item.price} ₸</small>
2454
  </div>
2455
  <div class="d-flex align-items-center">
2456
  <button class="btn btn-sm btn-outline-secondary cart-qty-btn" data-id="${id}" data-op="-1">-</button>
@@ -2460,7 +2609,7 @@ document.addEventListener('DOMContentLoaded', () => {
2460
  </div>
2461
  <div class="input-group input-group-sm mt-2">
2462
  <span class="input-group-text">Скидка</span>
2463
- <input type="text" class="form-control cart-discount-input" data-id="${id}" value="${item.discount}" inputmode="decimal">
2464
  </div>
2465
  </div>`;
2466
  }
@@ -2500,7 +2649,7 @@ document.addEventListener('DOMContentLoaded', () => {
2500
  ];
2501
 
2502
  prices.forEach(p => {
2503
- if (p.value !== undefined) {
2504
  const btn = document.createElement('button');
2505
  btn.type = 'button';
2506
  btn.className = 'btn btn-primary btn-lg';
@@ -2512,11 +2661,19 @@ document.addEventListener('DOMContentLoaded', () => {
2512
  container.appendChild(btn);
2513
  }
2514
  });
2515
- priceSelectModal.show();
 
 
 
 
 
2516
  };
2517
 
2518
  const handleProductSelection = (product) => {
2519
- const activeVariants = product.variants.filter(v => v.stock > 0);
 
 
 
2520
  if (activeVariants.length === 0) {
2521
  alert("У этого товара нет доступных вариантов.");
2522
  return;
@@ -2602,12 +2759,14 @@ document.addEventListener('DOMContentLoaded', () => {
2602
  }
2603
  });
2604
 
2605
- document.getElementById('clear-cart-btn').addEventListener('click', () => {
2606
- for(const id in cart) delete cart[id];
2607
- deliveryCost = 0;
2608
- transactionNote = '';
2609
- updateCartView();
2610
- });
 
 
2611
 
2612
  const productSearchInput = document.getElementById('product-search');
2613
  const productAccordionEl = document.getElementById('product-accordion');
@@ -2651,11 +2810,12 @@ document.addEventListener('DOMContentLoaded', () => {
2651
  let priceText = 'Нет в наличии';
2652
  if (p.variants && p.variants.length > 0) {
2653
  const activeVariants = p.variants.filter(v => v.stock > 0);
2654
- if (activeVariants.length > 0) {
2655
- if (activeVariants.length === 1) {
2656
- priceText = `${formatCurrencyJS(activeVariants[0].price_regular || activeVariants[0].price)} ₸`;
 
2657
  } else {
2658
- const prices = activeVariants.map(v => parseFloat(v.price_regular || v.price));
2659
  priceText = `от ${formatCurrencyJS(Math.min(...prices))} ₸`;
2660
  }
2661
  }
@@ -2670,8 +2830,8 @@ document.addEventListener('DOMContentLoaded', () => {
2670
  }).join('') : '<p class="text-muted text-center col-12">Товары не найдены.</p>';
2671
  });
2672
 
2673
- const completeSale = (paymentMethod) => {
2674
- if (!session.shift || !session.cashier || !session.kassa) {
2675
  alert('Смена не активна. Начните смену, чтобы проводить продажи.');
2676
  return;
2677
  }
@@ -2684,12 +2844,13 @@ document.addEventListener('DOMContentLoaded', () => {
2684
  headers: {'Content-Type': 'application/json'},
2685
  body: JSON.stringify({
2686
  cart: cart,
2687
- userId: session.cashier.id,
2688
- kassaId: session.kassa.id,
2689
- shiftId: session.shift.id,
2690
  paymentMethod: paymentMethod,
2691
  deliveryCost: deliveryCost,
2692
- note: transactionNote
 
2693
  })
2694
  })
2695
  .then(res => {
@@ -2704,6 +2865,10 @@ document.addEventListener('DOMContentLoaded', () => {
2704
  })
2705
  .then(data => {
2706
  if (data.success) {
 
 
 
 
2707
  for(const id in cart) delete cart[id];
2708
  deliveryCost = 0;
2709
  transactionNote = '';
@@ -2722,8 +2887,10 @@ document.addEventListener('DOMContentLoaded', () => {
2722
  });
2723
  };
2724
 
2725
- document.getElementById('pay-cash-btn').addEventListener('click', () => completeSale('cash'));
2726
- document.getElementById('pay-card-btn').addEventListener('click', () => completeSale('card'));
 
 
2727
 
2728
  const html5QrCode = new Html5Qrcode("reader");
2729
  const scannerStatusEl = document.getElementById('scanner-status');
@@ -2813,7 +2980,7 @@ document.addEventListener('DOMContentLoaded', () => {
2813
  localStorage.removeItem('current_shift');
2814
  startShiftModal.hide();
2815
  updateSessionUI();
2816
- cashierLoginModal.show();
2817
  };
2818
 
2819
  const handleStartShift = () => {
@@ -2903,11 +3070,13 @@ document.addEventListener('DOMContentLoaded', () => {
2903
 
2904
  updateSessionUI();
2905
 
2906
- if (!session.cashier) {
2907
- cashierLoginModal.show();
2908
- } else if (!session.shift) {
2909
- document.getElementById('start-shift-cashier-name').textContent = session.cashier.name;
2910
- startShiftModal.show();
 
 
2911
  }
2912
  };
2913
 
@@ -2925,7 +3094,7 @@ document.addEventListener('DOMContentLoaded', () => {
2925
  e.preventDefault();
2926
  const name = document.getElementById('custom-item-name').value || 'Товар без штрихкода';
2927
  const price = document.getElementById('custom-item-price').value;
2928
- const qty = parseInt(document.getElementById('custom-item-qty').value);
2929
 
2930
  if (parseLocaleNumber(price) > 0 && qty > 0) {
2931
  const customId = 'custom_' + Date.now();
@@ -2967,23 +3136,27 @@ document.addEventListener('DOMContentLoaded', () => {
2967
  .then(data => {
2968
  const count = data.length;
2969
  const badge = document.getElementById('held-bills-count');
2970
- if (count > 0) {
2971
- badge.textContent = count;
2972
- badge.style.display = '';
2973
- } else {
2974
- badge.style.display = 'none';
 
 
2975
  }
2976
  });
2977
  };
2978
 
2979
- document.getElementById('hold-bill-btn').addEventListener('click', () => {
2980
- if (Object.keys(cart).length === 0) {
2981
- alert('Корзина пуста. Нечего откладывать.');
2982
- return;
2983
- }
2984
- document.getElementById('hold-bill-form').reset();
2985
- holdBillModal.show();
2986
- });
 
 
2987
 
2988
  document.getElementById('hold-bill-form').addEventListener('submit', (e) => {
2989
  e.preventDefault();
@@ -3010,29 +3183,31 @@ document.addEventListener('DOMContentLoaded', () => {
3010
  });
3011
  });
3012
 
3013
- document.getElementById('list-held-bills-btn').addEventListener('click', () => {
3014
- fetch('/api/held_bills')
3015
- .then(res => res.json())
3016
- .then(bills => {
3017
- const listEl = document.getElementById('held-bills-list');
3018
- listEl.innerHTML = '';
3019
- if (bills.length === 0) {
3020
- listEl.innerHTML = '<p class="text-center text-muted">Нет отложенных накладных.</p>';
3021
- } else {
3022
- bills.forEach(bill => {
3023
- listEl.innerHTML += `
3024
- <div class="list-group-item d-flex justify-content-between align-items-center">
3025
- <span><strong>${bill.name}</strong> - ${new Date(bill.timestamp).toLocaleTimeString('ru-RU')}</span>
3026
- <div>
3027
- <button class="btn btn-sm btn-success restore-bill-btn" data-id="${bill.id}">Восстановить</button>
3028
- <button class="btn btn-sm btn-danger delete-held-bill-btn ms-2" data-id="${bill.id}">Удалить</button>
3029
- </div>
3030
- </div>`;
3031
- });
3032
- }
3033
- heldBillsListModal.show();
3034
- });
3035
- });
 
 
3036
 
3037
  document.getElementById('held-bills-list').addEventListener('click', (e) => {
3038
  const billId = e.target.dataset.id;
@@ -3086,7 +3261,15 @@ document.addEventListener('DOMContentLoaded', () => {
3086
 
3087
  INVENTORY_CONTENT = """
3088
  <div class="row mb-4">
3089
- <div class="col-md-4">
 
 
 
 
 
 
 
 
3090
  <div class="card text-center">
3091
  <div class="card-body">
3092
  <h6 class="card-subtitle mb-2 text-muted">Единиц товара на складе</h6>
@@ -3095,7 +3278,7 @@ INVENTORY_CONTENT = """
3095
  </div>
3096
  </div>
3097
  {% if session.admin_logged_in %}
3098
- <div class="col-md-4">
3099
  <div class="card text-center">
3100
  <div class="card-body">
3101
  <h6 class="card-subtitle mb-2 text-muted">Сумма по себестоимости</h6>
@@ -3103,7 +3286,7 @@ INVENTORY_CONTENT = """
3103
  </div>
3104
  </div>
3105
  </div>
3106
- <div class="col-md-4">
3107
  <div class="card text-center">
3108
  <div class="card-body">
3109
  <h6 class="card-subtitle mb-2 text-muted">Потенциальная прибыль</h6>
@@ -3165,8 +3348,8 @@ INVENTORY_CONTENT = """
3165
  <td><img src="{{ v.image_url if v.image_url else url_for('static', filename='placeholder.png') }}" class="img-thumbnail" style="width: 40px; height: 40px; object-fit: cover;"></td>
3166
  <td>{{ v.option_value }}</td>
3167
  <td>{{ format_currency_py(v.get('price_regular', v.get('price'))) }} ₸</td>
3168
- <td>{{ format_currency_py(v.get('price_min', '0.00')) }} ₸</td>
3169
- <td>{{ format_currency_py(v.get('price_wholesale', '0.00')) }} ₸</td>
3170
  {% if session.admin_logged_in %}<td>{{ format_currency_py(v.cost_price) }} ₸</td>{% endif %}
3171
  <td>{{ v.stock }}</td>
3172
  </tr>
@@ -3233,11 +3416,11 @@ INVENTORY_CONTENT = """
3233
  <div class="col-12 col-md-9">
3234
  <div class="row g-2">
3235
  <div class="col-12"><label>Название варианта</label><input type="text" name="variant_name[]" class="form-control" value="{{ v.option_value }}" required></div>
3236
- <div class="col-12 col-sm-4"><label>Цена Общая</label><input type="text" name="variant_price_regular[]" class="form-control" value="{{ v.get('price_regular', v.get('price'))|string|replace('.', ',') }}" inputmode="decimal"></div>
3237
- <div class="col-12 col-sm-4"><label>Цена Мин.</label><input type="text" name="variant_price_min[]" class="form-control" value="{{ v.get('price_min', '0.00')|string|replace('.', ',') }}" inputmode="decimal"></div>
3238
- <div class="col-12 col-sm-4"><label>Цена Опт.</label><input type="text" name="variant_price_wholesale[]" class="form-control" value="{{ v.get('price_wholesale', '0.00')|string|replace('.', ',') }}" inputmode="decimal"></div>
3239
- <div class="col-12 col-sm-6"><label>Себестоимость</label><input type="text" name="variant_cost_price[]" class="form-control" value="{{ v.cost_price|string|replace('.', ',') }}" inputmode="decimal"></div>
3240
- <div class="col-12 col-sm-6"><label>Остаток</label><input type="number" name="variant_stock[]" class="form-control" value="{{ v.stock }}"></div>
3241
  </div>
3242
  </div>
3243
  <div class="col-12 text-end">
@@ -3270,14 +3453,14 @@ INVENTORY_CONTENT = """
3270
  <tbody>
3271
  {% for p in inventory %}
3272
  {% for v in p.variants %}
3273
- {% if v.get('price_regular', v.get('price')) == '0.00' or v.cost_price == '0.00' %}
3274
  <tr>
3275
  <td class="align-middle">{{ p.name }} ({{ v.option_value }})</td>
3276
- <td><input type="text" name="price_regular[]" class="form-control form-control-sm" value="{{ v.get('price_regular', v.get('price'))|string|replace('.', ',') }}" inputmode="decimal"></td>
3277
- <td><input type="text" name="price_min[]" class="form-control form-control-sm" value="{{ v.get('price_min', '0.00')|string|replace('.', ',') }}" inputmode="decimal"></td>
3278
- <td><input type="text" name="price_wholesale[]" class="form-control form-control-sm" value="{{ v.get('price_wholesale', '0.00')|string|replace('.', ',') }}" inputmode="decimal"></td>
3279
  <td>
3280
- <input type="text" name="cost_price[]" class="form-control form-control-sm" value="{{ v.cost_price|string|replace('.', ',') }}" inputmode="decimal">
3281
  <input type="hidden" name="variant_id[]" value="{{ v.id }}">
3282
  </td>
3283
  </tr>
@@ -3324,12 +3507,12 @@ INVENTORY_CONTENT = """
3324
  <div class="row">
3325
  <div class="col-md-{% if session.admin_logged_in %}6{% else %}12{% endif %} mb-3">
3326
  <label for="stockin-quantity" class="form-label">Количество</label>
3327
- <input type="number" id="stockin-quantity" name="quantity" class="form-control" required min="1">
3328
  </div>
3329
  {% if session.admin_logged_in %}
3330
  <div class="col-md-6 mb-3">
3331
  <label for="stockin-cost" class="form-label">Себестоимость (за ед.)</label>
3332
- <input type="text" id="stockin-cost" name="cost_price" class="form-control" inputmode="decimal" placeholder="Необязательно">
3333
  </div>
3334
  {% endif %}
3335
  </div>
@@ -3528,9 +3711,9 @@ document.addEventListener('DOMContentLoaded', () => {
3528
  let adminInputs = '';
3529
  if (isAdmin) {
3530
  adminInputs = `
3531
- <div class="col-12 col-sm-4"><label>Цена Мин.</label><input type="text" name="variant_price_min[]" class="form-control" value="0,00" inputmode="decimal"></div>
3532
- <div class="col-12 col-sm-4"><label>Цена Опт.</label><input type="text" name="variant_price_wholesale[]" class="form-control" value="0,00" inputmode="decimal"></div>
3533
- <div class="col-12 col-sm-6"><label>Себестоимость</label><input type="text" name="variant_cost_price[]" class="form-control" value="0,00" inputmode="decimal"></div>
3534
  `;
3535
  }
3536
  div.innerHTML = `
@@ -3545,9 +3728,9 @@ document.addEventListener('DOMContentLoaded', () => {
3545
  <div class="col-12 col-md-9">
3546
  <div class="row g-2">
3547
  <div class="col-12"><label>Название варианта</label><input type="text" name="variant_name[]" class="form-control" placeholder="Напр. Синий, 42" required></div>
3548
- <div class="col-12 col-sm-4"><label>Цена Общая</label><input type="text" name="variant_price_regular[]" class="form-control" value="0,00" inputmode="decimal"></div>
3549
  ${adminInputs}
3550
- <div class="col-12 col-sm-6"><label>Остаток</label><input type="number" name="variant_stock[]" class="form-control" value="0"></div>
3551
  </div>
3552
  </div>
3553
  <div class="col-12 text-end">
@@ -3685,11 +3868,6 @@ TRANSACTIONS_CONTENT = """
3685
  </td>
3686
  <td>
3687
  {% if session.admin_logged_in %}
3688
- {% if t.type == 'sale' %}
3689
- <button class="btn btn-xs btn-outline-secondary py-0 px-1" data-bs-toggle="modal" data-bs-target="#editTransactionModal" data-transaction-id="{{t.id}}" data-transaction-items="{{t['items']|tojson}}">
3690
- <i class="fas fa-pencil-alt"></i>
3691
- </button>
3692
- {% endif %}
3693
  <form action="{{ url_for('delete_transaction', transaction_id=t.id) }}" method="POST" class="d-inline ms-1" onsubmit="return confirm('Удалить эту транзакцию? Действие необратимо.');">
3694
  <button type="submit" class="btn btn-xs btn-outline-danger py-0 px-1"><i class="fas fa-trash"></i></button>
3695
  </form>
@@ -3704,88 +3882,9 @@ TRANSACTIONS_CONTENT = """
3704
  </div>
3705
  </div>
3706
  </div>
3707
-
3708
- <div class="modal fade" id="editTransactionModal" tabindex="-1">
3709
- <div class="modal-dialog modal-lg">
3710
- <div class="modal-content">
3711
- <div class="modal-header"><h5 class="modal-title">Редактировать транзакцию</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
3712
- <div class="modal-body">
3713
- <p>ID: <strong id="edit-trans-id"></strong></p>
3714
- <form id="edit-trans-form">
3715
- <div id="edit-trans-items-container" class="table-responsive"></div>
3716
- </form>
3717
- </div>
3718
- <div class="modal-footer">
3719
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
3720
- <button type="button" id="save-trans-btn" class="btn btn-primary">Сохранить изменения</button>
3721
- </div>
3722
- </div>
3723
- </div>
3724
- </div>
3725
  """
3726
 
3727
- TRANSACTIONS_SCRIPTS = """
3728
- <script>
3729
- document.addEventListener('DOMContentLoaded', () => {
3730
- const editModal = new bootstrap.Modal(document.getElementById('editTransactionModal'));
3731
- const editModalEl = document.getElementById('editTransactionModal');
3732
- let currentTransactionId = null;
3733
-
3734
- editModalEl.addEventListener('show.bs.modal', event => {
3735
- const button = event.relatedTarget;
3736
- currentTransactionId = button.dataset.transactionId;
3737
- const items = JSON.parse(button.dataset.transactionItems);
3738
-
3739
- document.getElementById('edit-trans-id').textContent = currentTransactionId.substring(0, 8);
3740
- const container = document.getElementById('edit-trans-items-container');
3741
-
3742
- let tableHtml = `<table class="table table-sm"><thead><tr><th>Товар</th><th>Цена</th><th>Скидка</th></tr></thead><tbody>`;
3743
- items.forEach(item => {
3744
- const itemId = item.variant_id || item.product_id;
3745
- tableHtml += `
3746
- <tr data-item-id="${itemId}">
3747
- <td>${item.name} (${item.quantity} шт.)</td>
3748
- <td><input type="text" class="form-control form-control-sm" name="price" value="${String(item.price_at_sale).replace('.',',')}" inputmode="decimal"></td>
3749
- <td><input type="text" class="form-control form-control-sm" name="discount" value="${String(item.discount_per_item || '0').replace('.',',')}" inputmode="decimal"></td>
3750
- </tr>`;
3751
- });
3752
- tableHtml += `</tbody></table>`;
3753
- container.innerHTML = tableHtml;
3754
- });
3755
-
3756
- document.getElementById('save-trans-btn').addEventListener('click', () => {
3757
- const form = document.getElementById('edit-trans-form');
3758
- const items_update = [];
3759
- form.querySelectorAll('tbody tr').forEach(row => {
3760
- items_update.push({
3761
- id: row.dataset.itemId,
3762
- price: row.querySelector('input[name="price"]').value,
3763
- discount: row.querySelector('input[name="discount"]').value
3764
- });
3765
- });
3766
-
3767
- fetch(`/admin/transaction/edit/${currentTransactionId}`, {
3768
- method: 'POST',
3769
- headers: {'Content-Type': 'application/json'},
3770
- body: JSON.stringify({ items: items_update })
3771
- })
3772
- .then(res => res.json())
3773
- .then(data => {
3774
- if (data.success) {
3775
- editModal.hide();
3776
- window.location.reload();
3777
- } else {
3778
- alert('Ошибка: ' + data.message);
3779
- }
3780
- })
3781
- .catch(err => {
3782
- console.error(err);
3783
- alert('Сетевая ошибка.');
3784
- });
3785
- });
3786
- });
3787
- </script>
3788
- """
3789
 
3790
  REPORTS_CONTENT = """
3791
  <div class="card mb-4">
@@ -4203,7 +4302,7 @@ ADMIN_CONTENT = """
4203
  <input type="hidden" name="action" value="add">
4204
  <div class="input-group">
4205
  <input type="text" name="name" class="form-control" placeholder="Название кассы" required>
4206
- <input type="text" name="balance" class="form-control" placeholder="Начальный баланс" value="0" inputmode="decimal">
4207
  <button type="submit" class="btn btn-primary">Добавить</button>
4208
  </div>
4209
  </form>
@@ -4218,6 +4317,35 @@ ADMIN_CONTENT = """
4218
  </div>
4219
  </div>
4220
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4221
  <div class="col-12 mb-4">
4222
  <div class="card">
4223
  <div class="card-header"><h5 class="mb-0">Операции по кассе (Внесение/Изъятие)</h5></div>
@@ -4226,7 +4354,7 @@ ADMIN_CONTENT = """
4226
  <div class="row g-2 align-items-end">
4227
  <div class="col-md-3"><label class="form-label">Касса</label><select name="kassa_id" class="form-select" required><option value="">-- Выберите --</option>{% for k in kassas %}<option value="{{k.id}}">{{k.name}}</option>{% endfor %}</select></div>
4228
  <div class="col-md-2"><label class="form-label">Операция</label><select name="op_type" class="form-select" required><option value="deposit">Внесение</option><option value="withdrawal">Изъятие</option></select></div>
4229
- <div class="col-md-2"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal"></div>
4230
  <div class="col-md-3"><label class="form-label">Описание</label><input type="text" name="description" class="form-control"></div>
4231
  <div class="col-md-2"><button type="submit" class="btn btn-success w-100">Выполнить</button></div>
4232
  </div>
@@ -4240,7 +4368,7 @@ ADMIN_CONTENT = """
4240
  <div class="card-body d-flex flex-column">
4241
  <form action="{{ url_for('manage_expense') }}" method="POST" class="mb-4">
4242
  <div class="row g-2 align-items-end">
4243
- <div class="col-md-4"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal"></div>
4244
  <div class="col-md-8"><label class="form-label">Описание</label><input type="text" name="description" class="form-control" required placeholder="Напр: Аренда за май"></div>
4245
  <div class="col-12"><button type="submit" class="btn btn-warning w-100 mt-2">Добавить расход</button></div>
4246
  </div>
@@ -4276,7 +4404,7 @@ ADMIN_CONTENT = """
4276
  <div class="card-body d-flex flex-column">
4277
  <form action="{{ url_for('manage_personal_expense') }}" method="POST" class="mb-4">
4278
  <div class="row g-2 align-items-end">
4279
- <div class="col-md-4"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal"></div>
4280
  <div class="col-md-8"><label class="form-label">Описание</label><input type="text" name="description" class="form-control" required placeholder="Напр: Обед"></div>
4281
  <div class="col-12"><button type="submit" class="btn btn-info w-100 mt-2">Добавить расход</button></div>
4282
  </div>
@@ -4332,7 +4460,7 @@ ADMIN_CONTENT = """
4332
  <div class="mb-3"><label class="form-label">Имя</label><input type="text" name="name" class="form-control" required></div>
4333
  <div class="mb-3"><label class="form-label">ПИН-код</label><input type="password" name="pin" class="form-control" required></div>
4334
  <div class="mb-3"><label class="form-label">Тип оплаты</label><select name="payment_type" class="form-select"><option value="percentage">Процент от продаж</option><option value="salary">Фиксированная зарплата</option></select></div>
4335
- <div class="mb-3"><label class="form-label">Значение</label><input type="text" name="payment_value" class="form-control" inputmode="decimal" value="0" required><small class="form-text text-muted">Для процентов - число (напр. 5), для зарплаты - сумма в тенге.</small></div>
4336
  </div>
4337
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Сохранить</button></div>
4338
  </form>
@@ -4582,4 +4710,4 @@ if __name__ == '__main__':
4582
  backup_thread.start()
4583
  for key in DATA_FILES.keys():
4584
  load_json_data(key)
4585
- app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)
 
36
  HELD_BILLS_FILE = os.path.join(DATA_DIR, 'held_bills.json')
37
  CUSTOMERS_FILE = os.path.join(DATA_DIR, 'customers.json')
38
  STOCK_HISTORY_FILE = os.path.join(DATA_DIR, 'stock_history.json')
39
+ LINKS_FILE = os.path.join(DATA_DIR, 'links.json')
40
 
41
  DATA_FILES = {
42
  'inventory': (INVENTORY_FILE, threading.Lock()),
 
49
  'held_bills': (HELD_BILLS_FILE, threading.Lock()),
50
  'customers': (CUSTOMERS_FILE, threading.Lock()),
51
  'stock_history': (STOCK_HISTORY_FILE, threading.Lock()),
52
+ 'links': (LINKS_FILE, threading.Lock()),
53
  }
54
 
55
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE", "YOUR_WRITE_TOKEN_HERE")
 
69
  return str(obj)
70
  return json.JSONEncoder.default(self, obj)
71
 
72
+ def to_decimal(value_str, default='0'):
73
+ if value_str is None or str(value_str).strip() == '':
74
  return Decimal(default)
75
  try:
76
+ return Decimal(str(value_str).replace(',', '.').replace(' ', ''))
77
  except InvalidOperation:
78
  logging.warning(f"Could not convert '{value_str}' to Decimal. Returned {default}.")
79
  return Decimal(default)
 
160
  def format_currency_py(value):
161
  try:
162
  number = to_decimal(value)
163
+ return f"{number:,.0f}".replace(",", " ")
164
  except (InvalidOperation, TypeError, ValueError):
165
+ return "0"
166
 
167
  def generate_receipt_html(transaction):
168
  has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items', []))
 
235
  <p style="margin: 0; white-space: pre-wrap; font-size: 14px;">{safe_note}</p>
236
  </div>
237
  """
238
+
239
+ links = load_json_data('links')
240
+ links_html = ""
241
+ if links:
242
+ links_html = '<div class="print-hide" style="margin-top: 20px; text-align: center; border-top: 1px solid #ddd; padding-top: 15px;">'
243
+ for link in links:
244
+ links_html += f'<a href="{link["url"]}" target="_blank" style="display: inline-block; margin: 5px; padding: 10px 15px; background: #0d6efd; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">{link["name"]}</a>'
245
+ links_html += '</div>'
246
 
247
  return f"""
248
  <!DOCTYPE html>
 
283
  <body>
284
  <div class="invoice-box">
285
  <div class="print-hide" style="text-align: right; margin-bottom: 20px;">
286
+ <button onclick="window.print()" style="padding: 8px 12px; font-size: 14px; cursor: pointer; background: #6c757d; color: white; border: none; border-radius: 4px;">Печать</button>
287
+ <button onclick="editReceipt()" style="padding: 8px 12px; font-size: 14px; cursor: pointer; background: #ffc107; color: #000; border: none; border-radius: 4px; margin-left: 10px;">Изменить накладную</button>
288
  </div>
289
  <div class="header">
290
  <h1>Товарная накладная № {transaction['id'][:8]}</h1>
 
304
  <p>Способ оплаты: {'Наличные' if transaction['payment_method'] == 'cash' else 'Карта'}</p>
305
  <p>Кассир: {transaction['user_name']}</p>
306
  </div>
307
+ {links_html}
308
  </div>
309
+ <script>
310
+ function editReceipt() {{
311
+ let code = prompt("Введите ПИН-код кассира или пароль администратора для изменения накладной:");
312
+ if (!code) return;
313
+ fetch('/api/auth/universal', {{
314
+ method: 'POST',
315
+ headers: {{'Content-Type': 'application/json'}},
316
+ body: JSON.stringify({{code: code}})
317
+ }})
318
+ .then(r => r.json())
319
+ .then(d => {{
320
+ if (d.success) {{
321
+ window.location.href = '/?edit_tx={transaction["id"]}';
322
+ }} else {{
323
+ alert("Неверный пароль или ПИН-код");
324
+ }}
325
+ }});
326
+ }}
327
+ </script>
328
  </body>
329
  </html>
330
  """
 
342
  def inject_utils():
343
  return {'format_currency_py': format_currency_py, 'get_current_time': get_current_time, 'quote': quote}
344
 
345
+ @app.route('/api/auth/universal', methods=['POST'])
346
+ def auth_universal():
347
+ code = request.json.get('code')
348
+ if code == ADMIN_PASS:
349
+ return jsonify({'success': True})
350
+ user = find_user_by_pin(code)
351
+ if user:
352
+ return jsonify({'success': True})
353
+ return jsonify({'success': False})
354
+
355
  @app.route('/')
356
  def sales_screen():
357
  inventory = load_json_data('inventory')
358
  kassas = load_json_data('kassas')
359
 
360
+ edit_tx_id = request.args.get('edit_tx')
361
+ edit_tx = None
362
+ if edit_tx_id:
363
+ transactions = load_json_data('transactions')
364
+ edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
365
+
366
  active_inventory = []
367
  for p in inventory:
368
  if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants', [])):
 
378
  sorted_grouped_inventory = sorted(grouped_inventory.items())
379
 
380
  html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
381
+ return render_template_string(html, inventory=active_inventory, kassas=kassas, grouped_inventory=sorted_grouped_inventory, edit_tx=edit_tx)
382
 
383
  @app.route('/inventory', methods=['GET', 'POST'])
384
  def inventory_management():
 
412
  if not v_name: continue
413
 
414
  if is_admin:
415
+ v_price_regular = str(to_decimal(variant_prices_regular[i])) if i < len(variant_prices_regular) else '0'
416
+ v_price_min = str(to_decimal(variant_prices_min[i])) if i < len(variant_prices_min) else '0'
417
+ v_price_wholesale = str(to_decimal(variant_prices_wholesale[i])) if i < len(variant_prices_wholesale) else '0'
418
+ v_cost = str(to_decimal(variant_cost_prices[i])) if i < len(variant_cost_prices) else '0'
419
  else:
420
+ v_price_regular = '0'
421
+ v_price_min = '0'
422
+ v_price_wholesale = '0'
423
+ v_cost = '0'
424
 
425
  variants.append({
426
  'id': uuid.uuid4().hex,
 
459
  inventory_list.sort(key=lambda x: x.get('name', '').lower())
460
 
461
  total_units = 0
462
+ total_cost_value = Decimal('0')
463
+ total_retail_value = Decimal('0')
464
 
465
  for product in inventory_list:
466
  if isinstance(product, dict) and 'variants' in product:
 
476
  potential_profit = total_retail_value - total_cost_value
477
 
478
  inventory_summary = {
479
+ 'total_names': len(inventory_list),
480
  'total_units': total_units,
481
  'total_cost_value': total_cost_value,
482
  'potential_profit': potential_profit
 
635
 
636
  if old_stock + quantity > 0:
637
  avg_cost = ((old_cost * old_stock) + (new_cost * quantity) + delivery_cost) / (old_stock + quantity)
638
+ variant['cost_price'] = str(avg_cost.quantize(Decimal('1'), rounding=ROUND_HALF_UP))
639
 
640
  variant_found = True
641
  break
 
721
  try:
722
  data = request.get_json()
723
  cart = data.get('cart', {})
 
 
 
724
  payment_method = data.get('paymentMethod', 'cash')
725
  delivery_cost_str = data.get('deliveryCost', '0')
726
  note = data.get('note', '')
727
+ edit_tx_id = data.get('edit_tx_id')
 
 
728
 
729
  inventory = load_json_data('inventory')
730
  users = load_json_data('users')
731
  kassas = load_json_data('kassas')
732
+ transactions = load_json_data('transactions')
733
+
734
+ original_tx = None
735
+ if edit_tx_id:
736
+ original_tx = find_item_by_field(transactions, 'id', edit_tx_id)
737
+ if not original_tx:
738
+ return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
739
+
740
+ for item in original_tx.get('items', []):
741
+ if not item.get('is_custom'):
742
+ for p in inventory:
743
+ if p.get('id') == item.get('product_id'):
744
+ for v in p.get('variants', []):
745
+ if v.get('id') == item.get('variant_id'):
746
+ v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
747
+ break
748
+ break
749
+
750
+ if original_tx.get('payment_method') == 'cash':
751
+ for k in kassas:
752
+ if k.get('id') == original_tx.get('kassa_id'):
753
+ current_balance = to_decimal(k.get('balance', '0'))
754
+ amount = to_decimal(original_tx.get('total_amount', '0'))
755
+ k['balance'] = str(current_balance - amount)
756
+ k.setdefault('history', []).append({
757
+ 'type': 'correction_revert',
758
+ 'amount': str(-amount),
759
+ 'timestamp': get_current_time().isoformat(),
760
+ 'description': f"Отмена до изменения {edit_tx_id[:8]}"
761
+ })
762
+ break
763
+ else:
764
+ if not data.get('userId') or not data.get('kassaId') or not data.get('shiftId'):
765
+ return jsonify({'success': False, 'message': 'Неполные данные для продажи. Начните смену.'}), 400
766
+
767
+ user_id = data.get('userId') if data.get('userId') else original_tx.get('user_id')
768
+ kassa_id = data.get('kassaId') if data.get('kassaId') else original_tx.get('kassa_id')
769
+ shift_id = data.get('shiftId') if data.get('shiftId') else original_tx.get('shift_id')
770
+
771
+ if not cart:
772
+ return jsonify({'success': False, 'message': 'Корзина пуста.'}), 400
773
+
774
  user = find_item_by_field(users, 'id', user_id)
775
  kassa = find_item_by_field(kassas, 'id', kassa_id)
776
 
 
778
  return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
779
 
780
  sale_items = []
781
+ items_total = Decimal('0')
782
  inventory_updates = {}
783
 
784
  for item_id, cart_item in cart.items():
 
794
  'barcode': 'CUSTOM',
795
  'quantity': quantity_sold,
796
  'price_at_sale': str(price_at_sale),
797
+ 'cost_price_at_sale': '0',
798
+ 'discount_per_item': '0',
799
  'total': str(item_total),
800
  'is_custom': True
801
  })
 
827
  item_total = final_price * Decimal(quantity_sold)
828
  items_total += item_total
829
 
830
+ name_to_use = f"{product['name']} ({variant['option_value']})" if not edit_tx_id else cart_item.get('productName') or f"{product['name']} ({variant['option_value']})"
831
+
832
  sale_items.append({
833
  'product_id': product['id'],
834
  'variant_id': variant_id,
835
+ 'name': name_to_use,
836
  'barcode': product.get('barcode'),
837
  'quantity': quantity_sold,
838
  'price_at_sale': str(price_at_sale),
 
847
  now_iso = get_current_time().isoformat()
848
 
849
  new_transaction = {
850
+ 'id': edit_tx_id if edit_tx_id else uuid.uuid4().hex,
851
+ 'timestamp': original_tx['timestamp'] if original_tx else now_iso,
852
  'type': 'sale',
853
  'status': 'completed',
854
  'original_transaction_id': None,
 
864
  'payment_method': payment_method
865
  }
866
 
867
+ if edit_tx_id:
868
+ new_transaction['edits'] = original_tx.get('edits', []) + [{'timestamp': now_iso, 'type': 'full_edit'}]
869
+
870
  new_transaction['invoice_html'] = generate_receipt_html(new_transaction)
871
+
872
+ if edit_tx_id:
873
+ for i, t in enumerate(transactions):
874
+ if t.get('id') == edit_tx_id:
875
+ transactions[i] = new_transaction
876
+ break
877
+ else:
878
+ transactions.append(new_transaction)
879
 
880
  for variant_id, update_info in inventory_updates.items():
881
  for p in inventory:
 
895
  if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
896
  kassas[i]['history'] = []
897
  kassas[i]['history'].append({
898
+ 'type': 'sale' if not edit_tx_id else 'correction_apply',
899
  'amount': str(total_amount),
900
  'timestamp': now_iso,
901
+ 'transaction_id': new_transaction['id'],
902
+ 'description': 'Продажа' if not edit_tx_id else f'Измененная накладная {edit_tx_id[:8]}'
903
  })
904
  break
905
 
 
914
  receipt_url = url_for('view_receipt', transaction_id=new_transaction['id'], _external=True)
915
  return jsonify({
916
  'success': True,
917
+ 'message': 'Накладная успешно сохранена.' if edit_tx_id else 'Продажа успешно зарегистрирована.',
918
  'transactionId': new_transaction['id'],
919
  'receiptUrl': receipt_url
920
  })
 
993
  original_transaction = transactions[transaction_index]
994
  old_total_amount = to_decimal(original_transaction['total_amount'])
995
 
996
+ new_items_total = Decimal('0')
997
  updated_items = []
998
 
999
  for item in original_transaction['items']:
 
1285
  'timestamp': sh['timestamp'],
1286
  'type': 'stock_in',
1287
  'qty': sh['quantity'],
1288
+ 'price': sh.get('cost_price', '0'),
1289
  'doc_id': 'Приход',
1290
  'user': 'Админ',
1291
  'variant_name': f"{sh.get('product_name')} ({sh.get('variant_name')})"
 
1306
 
1307
  for product in inventory:
1308
  for variant in product.get('variants', []):
1309
+ total_revenue = Decimal('0')
1310
+ total_cogs = Decimal('0')
1311
  total_qty_sold = 0
1312
 
1313
  for t in transactions:
 
1350
  kassas = load_json_data('kassas')
1351
  expenses = load_json_data('expenses')
1352
  personal_expenses = load_json_data('personal_expenses')
1353
+ links = load_json_data('links')
1354
  expenses.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
1355
  personal_expenses.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
1356
  html = BASE_TEMPLATE.replace('__TITLE__', "Админ-панель").replace('__CONTENT__', ADMIN_CONTENT).replace('__SCRIPTS__', ADMIN_SCRIPTS)
1357
+ return render_template_string(html, users=users, kassas=kassas, expenses=expenses, personal_expenses=personal_expenses, links=links)
1358
 
1359
  @app.route('/admin/shifts')
1360
  @admin_required
 
1572
  flash("Личный расход не найден.", "warning")
1573
  return redirect(url_for('admin_panel'))
1574
 
1575
+ @app.route('/admin/link', methods=['POST'])
1576
+ @admin_required
1577
+ def manage_link():
1578
+ action = request.form.get('action')
1579
+ links = load_json_data('links')
1580
+ if action == 'add':
1581
+ name = request.form.get('name', '').strip()
1582
+ url = request.form.get('url', '').strip()
1583
+ if name and url:
1584
+ links.append({'id': uuid.uuid4().hex, 'name': name, 'url': url})
1585
+ flash("Ссылка добавлена.", "success")
1586
+ elif action == 'delete':
1587
+ link_id = request.form.get('id')
1588
+ links = [l for l in links if l.get('id') != link_id]
1589
+ flash("Ссылка удалена.", "success")
1590
+ save_json_data('links', links)
1591
+ upload_db_to_hf('links')
1592
+ return redirect(url_for('admin_panel'))
1593
+
1594
  @app.route('/cashier_login', methods=['GET', 'POST'])
1595
  def cashier_login():
1596
  if request.method == 'POST':
 
1765
  return redirect(url_for('cashier_login'))
1766
 
1767
  return_items = []
1768
+ total_return_amount = Decimal('0')
1769
  inventory_updates = {}
1770
  items_to_process = defaultdict(int)
1771
 
 
2146
  <div class="modal-dialog">
2147
  <div class="modal-content">
2148
  <div class="modal-header">
2149
+ <h5 class="modal-title">Готово</h5>
2150
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
2151
  </div>
2152
  <div class="modal-body">
2153
+ <p>Накладная успешно сохранена.</p>
2154
  <div class="mb-3">
2155
  <label for="whatsapp-phone" class="form-label">Отправить накладную на WhatsApp</label>
2156
  <div class="input-group">
 
2389
  <div id="session-info" class="small text-muted mt-1"></div>
2390
  </div>
2391
  <div class="card-body">
2392
+ <div id="edit-mode-banner" class="alert alert-warning mb-2 p-2" style="display:none;">
2393
+ <strong>Режим редактирования:</strong> Накладная <span id="edit-tx-id-display"></span>
2394
+ <button class="btn btn-sm btn-danger float-end py-0" onclick="window.location.href='/'">Отмена</button>
2395
+ </div>
2396
  <div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
2397
  <div class="mb-3">
2398
+ <div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
2399
+ <div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
2400
  <hr class="my-1">
2401
+ <div class="d-flex justify-content-between align-items-center h4"><span>Итого:</span><span id="cart-total">0 ₸</span></div>
2402
  </div>
2403
  <div class="btn-group w-100 mb-2">
2404
  <button class="btn btn-outline-secondary btn-sm" id="add-delivery-btn"><i class="fas fa-truck"></i> Доставка</button>
2405
  <button class="btn btn-outline-secondary btn-sm" id="add-note-btn"><i class="fas fa-sticky-note"></i> Заметка</button>
2406
  </div>
2407
+ <div class="d-grid gap-2" id="payment-buttons-container">
2408
  <div class="btn-group"><button class="btn btn-success flex-grow-1" id="pay-cash-btn"><i class="fas fa-money-bill-wave me-2"></i>Наличные</button><button class="btn btn-info flex-grow-1" id="pay-card-btn"><i class="far fa-credit-card me-2"></i>Карта</button></div>
2409
  <div class="btn-group">
2410
  <button class="btn btn-secondary" id="hold-bill-btn"><i class="fas fa-pause me-2"></i>Отложить накладную</button>
 
2458
  <form id="custom-item-form">
2459
  <div class="modal-body">
2460
  <div class="mb-3"><label class="form-label">Название (необязательно)</label><input type="text" id="custom-item-name" class="form-control" placeholder="Напр. 'Пакет'"></div>
2461
+ <div class="mb-3"><label class="form-label">Цена за 1 шт.</label><input type="text" id="custom-item-price" class="form-control" inputmode="decimal" placeholder="0" required></div>
2462
+ <div class="mb-3"><label class="form-label">Количество</label><input type="number" id="custom-item-qty" class="form-control" value="" placeholder="1" min="1" required></div>
2463
  </div>
2464
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Добавить в накладную</button></div>
2465
  </form>
 
2525
  shift: null
2526
  };
2527
 
2528
+ const editTx = {{ edit_tx|tojson|safe if edit_tx else 'null' }};
2529
+ if (editTx) {
2530
+ document.getElementById('edit-mode-banner').style.display = 'block';
2531
+ document.getElementById('edit-tx-id-display').textContent = editTx.id.substring(0,8);
2532
+ editTx.items.forEach(item => {
2533
+ let variantName = '';
2534
+ if (item.name.includes('(')) {
2535
+ variantName = item.name.substring(item.name.indexOf('(')+1, item.name.lastIndexOf(')'));
2536
+ }
2537
+ cart[item.variant_id] = {
2538
+ productId: item.product_id,
2539
+ productName: item.name,
2540
+ variantName: variantName,
2541
+ price: String(item.price_at_sale).replace('.', ','),
2542
+ quantity: item.quantity,
2543
+ discount: String(item.discount_per_item || '0').replace('.', ','),
2544
+ isCustom: item.is_custom || false
2545
+ };
2546
+ });
2547
+ deliveryCost = parseFloat(editTx.delivery_cost || 0);
2548
+ transactionNote = editTx.note || '';
2549
+
2550
+ const paymentContainer = document.getElementById('payment-buttons-container');
2551
+ paymentContainer.innerHTML = `<button class="btn btn-warning w-100 btn-lg mb-2" id="save-edit-btn"><i class="fas fa-save me-2"></i>Сохранить изменения</button>`;
2552
+ document.getElementById('save-edit-btn').addEventListener('click', () => {
2553
+ completeSale(editTx.payment_method, editTx.id);
2554
+ });
2555
+ }
2556
+
2557
  function playBeep() {
2558
  if (!audioCtx) {
2559
  try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
 
2578
  const formatCurrencyJS = (value) => {
2579
  try {
2580
  const number = parseFloat(String(value).replace(/\\s/g, '').replace(',', '.'));
2581
+ if (isNaN(number)) return '0';
2582
+ return number.toLocaleString('ru-RU', {minimumFractionDigits: 0, maximumFractionDigits: 0});
2583
  } catch (e) {
2584
+ return '0';
2585
  }
2586
  };
2587
 
 
2598
  <div class="list-group-item">
2599
  <div class="d-flex justify-content-between align-items-start">
2600
  <div>
2601
+ <h6 class="mb-0 small">${item.productName} ${item.variantName && !item.isCustom && !item.productName.includes(item.variantName) ? '('+item.variantName+')' : ''}</h6>
2602
+ <small>${formatCurrencyJS(item.price)} ₸</small>
2603
  </div>
2604
  <div class="d-flex align-items-center">
2605
  <button class="btn btn-sm btn-outline-secondary cart-qty-btn" data-id="${id}" data-op="-1">-</button>
 
2609
  </div>
2610
  <div class="input-group input-group-sm mt-2">
2611
  <span class="input-group-text">Скидка</span>
2612
+ <input type="text" class="form-control cart-discount-input" data-id="${id}" value="${item.discount}" inputmode="decimal" placeholder="0">
2613
  </div>
2614
  </div>`;
2615
  }
 
2649
  ];
2650
 
2651
  prices.forEach(p => {
2652
+ if (p.value !== undefined && p.value !== '0' && p.value !== '0.00' && p.value !== '') {
2653
  const btn = document.createElement('button');
2654
  btn.type = 'button';
2655
  btn.className = 'btn btn-primary btn-lg';
 
2661
  container.appendChild(btn);
2662
  }
2663
  });
2664
+
2665
+ if (container.innerHTML === '') {
2666
+ addToCart(product, variant, variant.price_regular || variant.price || '0', 'Общая');
2667
+ } else {
2668
+ priceSelectModal.show();
2669
+ }
2670
  };
2671
 
2672
  const handleProductSelection = (product) => {
2673
+ let activeVariants = product.variants;
2674
+ if (!editTx) {
2675
+ activeVariants = product.variants.filter(v => v.stock > 0);
2676
+ }
2677
  if (activeVariants.length === 0) {
2678
  alert("У этого товара нет доступных вариантов.");
2679
  return;
 
2759
  }
2760
  });
2761
 
2762
+ if (document.getElementById('clear-cart-btn')) {
2763
+ document.getElementById('clear-cart-btn').addEventListener('click', () => {
2764
+ for(const id in cart) delete cart[id];
2765
+ deliveryCost = 0;
2766
+ transactionNote = '';
2767
+ updateCartView();
2768
+ });
2769
+ }
2770
 
2771
  const productSearchInput = document.getElementById('product-search');
2772
  const productAccordionEl = document.getElementById('product-accordion');
 
2810
  let priceText = 'Нет в наличии';
2811
  if (p.variants && p.variants.length > 0) {
2812
  const activeVariants = p.variants.filter(v => v.stock > 0);
2813
+ if (activeVariants.length > 0 || editTx) {
2814
+ let variantsToUse = editTx ? p.variants : activeVariants;
2815
+ if (variantsToUse.length === 1) {
2816
+ priceText = `${formatCurrencyJS(variantsToUse[0].price_regular || variantsToUse[0].price)} ₸`;
2817
  } else {
2818
+ const prices = variantsToUse.map(v => parseFloat(v.price_regular || v.price));
2819
  priceText = `от ${formatCurrencyJS(Math.min(...prices))} ₸`;
2820
  }
2821
  }
 
2830
  }).join('') : '<p class="text-muted text-center col-12">Товары не найдены.</p>';
2831
  });
2832
 
2833
+ const completeSale = (paymentMethod, editTxId = null) => {
2834
+ if (!editTxId && (!session.shift || !session.cashier || !session.kassa)) {
2835
  alert('Смена не активна. Начните смену, чтобы проводить продажи.');
2836
  return;
2837
  }
 
2844
  headers: {'Content-Type': 'application/json'},
2845
  body: JSON.stringify({
2846
  cart: cart,
2847
+ userId: session.cashier ? session.cashier.id : (editTx ? editTx.user_id : ''),
2848
+ kassaId: session.kassa ? session.kassa.id : (editTx ? editTx.kassa_id : ''),
2849
+ shiftId: session.shift ? session.shift.id : (editTx ? editTx.shift_id : ''),
2850
  paymentMethod: paymentMethod,
2851
  deliveryCost: deliveryCost,
2852
+ note: transactionNote,
2853
+ edit_tx_id: editTxId
2854
  })
2855
  })
2856
  .then(res => {
 
2865
  })
2866
  .then(data => {
2867
  if (data.success) {
2868
+ if (editTxId) {
2869
+ window.location.href = data.receiptUrl;
2870
+ return;
2871
+ }
2872
  for(const id in cart) delete cart[id];
2873
  deliveryCost = 0;
2874
  transactionNote = '';
 
2887
  });
2888
  };
2889
 
2890
+ if (document.getElementById('pay-cash-btn')) {
2891
+ document.getElementById('pay-cash-btn').addEventListener('click', () => completeSale('cash'));
2892
+ document.getElementById('pay-card-btn').addEventListener('click', () => completeSale('card'));
2893
+ }
2894
 
2895
  const html5QrCode = new Html5Qrcode("reader");
2896
  const scannerStatusEl = document.getElementById('scanner-status');
 
2980
  localStorage.removeItem('current_shift');
2981
  startShiftModal.hide();
2982
  updateSessionUI();
2983
+ if (!editTx) cashierLoginModal.show();
2984
  };
2985
 
2986
  const handleStartShift = () => {
 
3070
 
3071
  updateSessionUI();
3072
 
3073
+ if (!editTx) {
3074
+ if (!session.cashier) {
3075
+ cashierLoginModal.show();
3076
+ } else if (!session.shift) {
3077
+ document.getElementById('start-shift-cashier-name').textContent = session.cashier.name;
3078
+ startShiftModal.show();
3079
+ }
3080
  }
3081
  };
3082
 
 
3094
  e.preventDefault();
3095
  const name = document.getElementById('custom-item-name').value || 'Товар без штрихкода';
3096
  const price = document.getElementById('custom-item-price').value;
3097
+ const qty = parseInt(document.getElementById('custom-item-qty').value || 1);
3098
 
3099
  if (parseLocaleNumber(price) > 0 && qty > 0) {
3100
  const customId = 'custom_' + Date.now();
 
3136
  .then(data => {
3137
  const count = data.length;
3138
  const badge = document.getElementById('held-bills-count');
3139
+ if (badge) {
3140
+ if (count > 0) {
3141
+ badge.textContent = count;
3142
+ badge.style.display = '';
3143
+ } else {
3144
+ badge.style.display = 'none';
3145
+ }
3146
  }
3147
  });
3148
  };
3149
 
3150
+ if (document.getElementById('hold-bill-btn')) {
3151
+ document.getElementById('hold-bill-btn').addEventListener('click', () => {
3152
+ if (Object.keys(cart).length === 0) {
3153
+ alert('Корзина пуста. Нечего откладывать.');
3154
+ return;
3155
+ }
3156
+ document.getElementById('hold-bill-form').reset();
3157
+ holdBillModal.show();
3158
+ });
3159
+ }
3160
 
3161
  document.getElementById('hold-bill-form').addEventListener('submit', (e) => {
3162
  e.preventDefault();
 
3183
  });
3184
  });
3185
 
3186
+ if (document.getElementById('list-held-bills-btn')) {
3187
+ document.getElementById('list-held-bills-btn').addEventListener('click', () => {
3188
+ fetch('/api/held_bills')
3189
+ .then(res => res.json())
3190
+ .then(bills => {
3191
+ const listEl = document.getElementById('held-bills-list');
3192
+ listEl.innerHTML = '';
3193
+ if (bills.length === 0) {
3194
+ listEl.innerHTML = '<p class="text-center text-muted">Нет отложенных накладных.</p>';
3195
+ } else {
3196
+ bills.forEach(bill => {
3197
+ listEl.innerHTML += `
3198
+ <div class="list-group-item d-flex justify-content-between align-items-center">
3199
+ <span><strong>${bill.name}</strong> - ${new Date(bill.timestamp).toLocaleTimeString('ru-RU')}</span>
3200
+ <div>
3201
+ <button class="btn btn-sm btn-success restore-bill-btn" data-id="${bill.id}">Восстановить</button>
3202
+ <button class="btn btn-sm btn-danger delete-held-bill-btn ms-2" data-id="${bill.id}">Удалить</button>
3203
+ </div>
3204
+ </div>`;
3205
+ });
3206
+ }
3207
+ heldBillsListModal.show();
3208
+ });
3209
+ });
3210
+ }
3211
 
3212
  document.getElementById('held-bills-list').addEventListener('click', (e) => {
3213
  const billId = e.target.dataset.id;
 
3261
 
3262
  INVENTORY_CONTENT = """
3263
  <div class="row mb-4">
3264
+ <div class="col-md-3">
3265
+ <div class="card text-center">
3266
+ <div class="card-body">
3267
+ <h6 class="card-subtitle mb-2 text-muted">Наименований</h6>
3268
+ <h4 class="card-title">{{ inventory_summary.total_names }} шт.</h4>
3269
+ </div>
3270
+ </div>
3271
+ </div>
3272
+ <div class="col-md-3">
3273
  <div class="card text-center">
3274
  <div class="card-body">
3275
  <h6 class="card-subtitle mb-2 text-muted">Единиц товара на складе</h6>
 
3278
  </div>
3279
  </div>
3280
  {% if session.admin_logged_in %}
3281
+ <div class="col-md-3">
3282
  <div class="card text-center">
3283
  <div class="card-body">
3284
  <h6 class="card-subtitle mb-2 text-muted">Сумма по себестоимости</h6>
 
3286
  </div>
3287
  </div>
3288
  </div>
3289
+ <div class="col-md-3">
3290
  <div class="card text-center">
3291
  <div class="card-body">
3292
  <h6 class="card-subtitle mb-2 text-muted">Потенциальная прибыль</h6>
 
3348
  <td><img src="{{ v.image_url if v.image_url else url_for('static', filename='placeholder.png') }}" class="img-thumbnail" style="width: 40px; height: 40px; object-fit: cover;"></td>
3349
  <td>{{ v.option_value }}</td>
3350
  <td>{{ format_currency_py(v.get('price_regular', v.get('price'))) }} ₸</td>
3351
+ <td>{{ format_currency_py(v.get('price_min', '0')) }} ₸</td>
3352
+ <td>{{ format_currency_py(v.get('price_wholesale', '0')) }} ₸</td>
3353
  {% if session.admin_logged_in %}<td>{{ format_currency_py(v.cost_price) }} ₸</td>{% endif %}
3354
  <td>{{ v.stock }}</td>
3355
  </tr>
 
3416
  <div class="col-12 col-md-9">
3417
  <div class="row g-2">
3418
  <div class="col-12"><label>Название варианта</label><input type="text" name="variant_name[]" class="form-control" value="{{ v.option_value }}" required></div>
3419
+ <div class="col-12 col-sm-4"><label>Цена Общая</label><input type="text" name="variant_price_regular[]" class="form-control" value="{{ v.get('price_regular', v.get('price'))|string|replace('.', ',') if v.get('price_regular', v.get('price')) != '0' else '' }}" placeholder="0" inputmode="decimal"></div>
3420
+ <div class="col-12 col-sm-4"><label>Цена Мин.</label><input type="text" name="variant_price_min[]" class="form-control" value="{{ v.get('price_min', '0')|string|replace('.', ',') if v.get('price_min', '0') != '0' else '' }}" placeholder="0" inputmode="decimal"></div>
3421
+ <div class="col-12 col-sm-4"><label>Цена Опт.</label><input type="text" name="variant_price_wholesale[]" class="form-control" value="{{ v.get('price_wholesale', '0')|string|replace('.', ',') if v.get('price_wholesale', '0') != '0' else '' }}" placeholder="0" inputmode="decimal"></div>
3422
+ <div class="col-12 col-sm-6"><label>Себестоимость</label><input type="text" name="variant_cost_price[]" class="form-control" value="{{ v.cost_price|string|replace('.', ',') if v.cost_price != '0' else '' }}" placeholder="0" inputmode="decimal"></div>
3423
+ <div class="col-12 col-sm-6"><label>Остаток</label><input type="number" name="variant_stock[]" class="form-control" value="{{ v.stock if v.stock != 0 else '' }}" placeholder="0"></div>
3424
  </div>
3425
  </div>
3426
  <div class="col-12 text-end">
 
3453
  <tbody>
3454
  {% for p in inventory %}
3455
  {% for v in p.variants %}
3456
+ {% if v.get('price_regular', v.get('price', '0')) == '0' or v.get('price_regular', v.get('price', '0.00')) == '0.00' or v.cost_price == '0' or v.cost_price == '0.00' %}
3457
  <tr>
3458
  <td class="align-middle">{{ p.name }} ({{ v.option_value }})</td>
3459
+ <td><input type="text" name="price_regular[]" class="form-control form-control-sm" value="" placeholder="0" inputmode="decimal"></td>
3460
+ <td><input type="text" name="price_min[]" class="form-control form-control-sm" value="" placeholder="0" inputmode="decimal"></td>
3461
+ <td><input type="text" name="price_wholesale[]" class="form-control form-control-sm" value="" placeholder="0" inputmode="decimal"></td>
3462
  <td>
3463
+ <input type="text" name="cost_price[]" class="form-control form-control-sm" value="" placeholder="0" inputmode="decimal">
3464
  <input type="hidden" name="variant_id[]" value="{{ v.id }}">
3465
  </td>
3466
  </tr>
 
3507
  <div class="row">
3508
  <div class="col-md-{% if session.admin_logged_in %}6{% else %}12{% endif %} mb-3">
3509
  <label for="stockin-quantity" class="form-label">Количество</label>
3510
+ <input type="number" id="stockin-quantity" name="quantity" class="form-control" placeholder="1" required min="1">
3511
  </div>
3512
  {% if session.admin_logged_in %}
3513
  <div class="col-md-6 mb-3">
3514
  <label for="stockin-cost" class="form-label">Себестоимость (за ед.)</label>
3515
+ <input type="text" id="stockin-cost" name="cost_price" class="form-control" inputmode="decimal" placeholder="0">
3516
  </div>
3517
  {% endif %}
3518
  </div>
 
3711
  let adminInputs = '';
3712
  if (isAdmin) {
3713
  adminInputs = `
3714
+ <div class="col-12 col-sm-4"><label>Цена Мин.</label><input type="text" name="variant_price_min[]" class="form-control" value="" placeholder="0" inputmode="decimal"></div>
3715
+ <div class="col-12 col-sm-4"><label>Цена Опт.</label><input type="text" name="variant_price_wholesale[]" class="form-control" value="" placeholder="0" inputmode="decimal"></div>
3716
+ <div class="col-12 col-sm-6"><label>Се��естоимость</label><input type="text" name="variant_cost_price[]" class="form-control" value="" placeholder="0" inputmode="decimal"></div>
3717
  `;
3718
  }
3719
  div.innerHTML = `
 
3728
  <div class="col-12 col-md-9">
3729
  <div class="row g-2">
3730
  <div class="col-12"><label>Название варианта</label><input type="text" name="variant_name[]" class="form-control" placeholder="Напр. Синий, 42" required></div>
3731
+ <div class="col-12 col-sm-4"><label>Цена Общая</label><input type="text" name="variant_price_regular[]" class="form-control" value="" placeholder="0" inputmode="decimal"></div>
3732
  ${adminInputs}
3733
+ <div class="col-12 col-sm-6"><label>Остаток</label><input type="number" name="variant_stock[]" class="form-control" value="" placeholder="0"></div>
3734
  </div>
3735
  </div>
3736
  <div class="col-12 text-end">
 
3868
  </td>
3869
  <td>
3870
  {% if session.admin_logged_in %}
 
 
 
 
 
3871
  <form action="{{ url_for('delete_transaction', transaction_id=t.id) }}" method="POST" class="d-inline ms-1" onsubmit="return confirm('Удалить эту транзакцию? Действие необратимо.');">
3872
  <button type="submit" class="btn btn-xs btn-outline-danger py-0 px-1"><i class="fas fa-trash"></i></button>
3873
  </form>
 
3882
  </div>
3883
  </div>
3884
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3885
  """
3886
 
3887
+ TRANSACTIONS_SCRIPTS = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3888
 
3889
  REPORTS_CONTENT = """
3890
  <div class="card mb-4">
 
4302
  <input type="hidden" name="action" value="add">
4303
  <div class="input-group">
4304
  <input type="text" name="name" class="form-control" placeholder="Название кассы" required>
4305
+ <input type="text" name="balance" class="form-control" placeholder="Начальный баланс" value="" inputmode="decimal">
4306
  <button type="submit" class="btn btn-primary">Добавить</button>
4307
  </div>
4308
  </form>
 
4317
  </div>
4318
  </div>
4319
  </div>
4320
+
4321
+ <div class="col-12 mb-4">
4322
+ <div class="card">
4323
+ <div class="card-header"><h5 class="mb-0">Ссылки в накладных</h5></div>
4324
+ <div class="card-body">
4325
+ <form action="{{ url_for('manage_link') }}" method="POST" class="mb-3">
4326
+ <input type="hidden" name="action" value="add">
4327
+ <div class="row g-2 align-items-end">
4328
+ <div class="col-md-5"><label class="form-label">Название (текст ссылки)</label><input type="text" name="name" class="form-control" placeholder="Напр: Наш Instagram" required></div>
4329
+ <div class="col-md-5"><label class="form-label">URL (адрес)</label><input type="url" name="url" class="form-control" placeholder="https://..." required></div>
4330
+ <div class="col-md-2"><button type="submit" class="btn btn-primary w-100">Добавить</button></div>
4331
+ </div>
4332
+ </form>
4333
+ <ul class="list-group">
4334
+ {% for l in links %}
4335
+ <li class="list-group-item d-flex justify-content-between align-items-center">
4336
+ <a href="{{ l.url }}" target="_blank">{{ l.name }}</a>
4337
+ <form action="{{ url_for('manage_link') }}" method="POST" onsubmit="return confirm('Удалить ссылку?');">
4338
+ <input type="hidden" name="action" value="delete">
4339
+ <input type="hidden" name="id" value="{{ l.id }}">
4340
+ <button type="submit" class="btn btn-sm btn-danger"><i class="fas fa-trash"></i></button>
4341
+ </form>
4342
+ </li>
4343
+ {% endfor %}
4344
+ </ul>
4345
+ </div>
4346
+ </div>
4347
+ </div>
4348
+
4349
  <div class="col-12 mb-4">
4350
  <div class="card">
4351
  <div class="card-header"><h5 class="mb-0">Операции по кассе (Внесение/Изъятие)</h5></div>
 
4354
  <div class="row g-2 align-items-end">
4355
  <div class="col-md-3"><label class="form-label">Касса</label><select name="kassa_id" class="form-select" required><option value="">-- Выберите --</option>{% for k in kassas %}<option value="{{k.id}}">{{k.name}}</option>{% endfor %}</select></div>
4356
  <div class="col-md-2"><label class="form-label">Операция</label><select name="op_type" class="form-select" required><option value="deposit">Внесение</option><option value="withdrawal">Изъятие</option></select></div>
4357
+ <div class="col-md-2"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal" placeholder="0"></div>
4358
  <div class="col-md-3"><label class="form-label">Описание</label><input type="text" name="description" class="form-control"></div>
4359
  <div class="col-md-2"><button type="submit" class="btn btn-success w-100">Выполнить</button></div>
4360
  </div>
 
4368
  <div class="card-body d-flex flex-column">
4369
  <form action="{{ url_for('manage_expense') }}" method="POST" class="mb-4">
4370
  <div class="row g-2 align-items-end">
4371
+ <div class="col-md-4"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal" placeholder="0"></div>
4372
  <div class="col-md-8"><label class="form-label">Описание</label><input type="text" name="description" class="form-control" required placeholder="Напр: Аренда за май"></div>
4373
  <div class="col-12"><button type="submit" class="btn btn-warning w-100 mt-2">Добавить расход</button></div>
4374
  </div>
 
4404
  <div class="card-body d-flex flex-column">
4405
  <form action="{{ url_for('manage_personal_expense') }}" method="POST" class="mb-4">
4406
  <div class="row g-2 align-items-end">
4407
+ <div class="col-md-4"><label class="form-label">Сумма</label><input type="text" name="amount" class="form-control" required inputmode="decimal" placeholder="0"></div>
4408
  <div class="col-md-8"><label class="form-label">Описание</label><input type="text" name="description" class="form-control" required placeholder="Напр: Обед"></div>
4409
  <div class="col-12"><button type="submit" class="btn btn-info w-100 mt-2">Добавить расход</button></div>
4410
  </div>
 
4460
  <div class="mb-3"><label class="form-label">Имя</label><input type="text" name="name" class="form-control" required></div>
4461
  <div class="mb-3"><label class="form-label">ПИН-код</label><input type="password" name="pin" class="form-control" required></div>
4462
  <div class="mb-3"><label class="form-label">Тип оплаты</label><select name="payment_type" class="form-select"><option value="percentage">Процент от продаж</option><option value="salary">Фиксированная зарплата</option></select></div>
4463
+ <div class="mb-3"><label class="form-label">Значение</label><input type="text" name="payment_value" class="form-control" inputmode="decimal" placeholder="0" required><small class="form-text text-muted">Для процентов - число (напр. 5), для зарплаты - сумма в тенге.</small></div>
4464
  </div>
4465
  <div class="modal-footer"><button type="submit" class="btn btn-primary">Сохранить</button></div>
4466
  </form>
 
4710
  backup_thread.start()
4711
  for key in DATA_FILES.keys():
4712
  load_json_data(key)
4713
+ app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)