Kgshop commited on
Commit
f702da5
·
verified ·
1 Parent(s): 227afd2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -416
app.py CHANGED
@@ -235,15 +235,11 @@ def load_data():
235
  if 'variants' not in product: product['variants'] =[]; changed = True
236
  if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True
237
  if 'stock' not in product: product['stock'] = ""; changed = True
238
- if 'is_available' not in product: product['is_available'] = True; changed = True
239
- if 'volume_prices' not in product: product['volume_prices'] = []; changed = True
240
  for v in product['variants']:
241
  if 'stock' not in v: v['stock'] = ""; changed = True
242
  if 'box_price' not in v: v['box_price'] = ""; changed = True
243
  if 'barcode' not in v: v['barcode'] = ""; changed = True
244
  if 'pieces_per_box' not in v: v['pieces_per_box'] = product.get('pieces_per_box', ""); changed = True
245
- if 'is_available' not in v: v['is_available'] = True; changed = True
246
- if 'volume_prices' not in v: v['volume_prices'] = []; changed = True
247
 
248
  for order_id, order in env_data['orders'].items():
249
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
@@ -253,7 +249,6 @@ def load_data():
253
  for item in order.get('cart', []):
254
  if 'discount' not in item: item['discount'] = 0; changed = True
255
  if 'category' not in item: item['category'] = 'Без категории'; changed = True
256
- if 'volume_prices' not in item: item['volume_prices'] = []; changed = True
257
 
258
  if changed or not os.path.exists(DATA_FILE):
259
  try:
@@ -318,14 +313,6 @@ def save_env_data(env_id, env_data):
318
  all_data[env_id] = env_data
319
  save_data(all_data)
320
 
321
- def get_applicable_price(base_price, volume_prices, qty):
322
- if not volume_prices:
323
- return base_price
324
- for vp in sorted(volume_prices, key=lambda x: x['qty'], reverse=True):
325
- if qty >= vp['qty']:
326
- return vp['price']
327
- return base_price
328
-
329
  def update_order_totals(order, business_type):
330
  total = 0
331
  global_discount = float(order.get('global_discount', 0))
@@ -337,15 +324,13 @@ def update_order_totals(order, business_type):
337
  c_price = float(i.get('price', 0))
338
  c_box_price = float(i.get('cart_box_price', 0))
339
  item_discount = float(i.get('discount', 0))
340
- volume_prices = i.get('volume_prices', [])
341
 
342
- unit = c_price
343
- if business_type == 'wholesale' and volume_prices:
344
- unit = get_applicable_price(c_price, volume_prices, qty)
345
- elif business_type in ['mixed', 'wholesale'] and c_box_price > 0 and ppb > 1 and qty >= ppb:
346
- unit = c_box_price / ppb
347
 
348
- discounted_price = max(0, unit - item_discount)
349
  item_total = discounted_price * qty
350
  i['calculated_price'] = round(discounted_price, 2)
351
  total += item_total
@@ -692,7 +677,6 @@ CATALOG_TEMPLATE = '''
692
  .process-return-btn { background: #e17055; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; }
693
 
694
  .history-btn { background: #0984e3; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; text-decoration: none; display: block; text-align: center; box-sizing: border-box; }
695
- .unavailable-item { opacity: 0.5; filter: grayscale(1); }
696
 
697
  @media (min-width: 768px) {
698
  .categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
@@ -987,7 +971,6 @@ CATALOG_TEMPLATE = '''
987
  if (p.variants && p.variants.length > 0) {
988
  variantsHtml = `<div class="variants-list">`;
989
  p.variants.forEach((v, idx) => {
990
- let vClass = v.is_available === false ? 'unavailable-item' : '';
991
  let vPrice = p.has_variant_prices ? v.price : p.price;
992
  let vBoxPrice = p.has_variant_prices ? (v.box_price || '') : (p.box_price || '');
993
  let vStockHtml = showStock && v.stock !== "" && v.stock !== null ? `<div class="variant-stock">Остаток: ${v.stock} шт</div>` : '';
@@ -1000,39 +983,25 @@ CATALOG_TEMPLATE = '''
1000
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${vBoxPrice} ${currency}</span>`;
1001
  }
1002
 
1003
- let vVolHtml = '';
1004
- if (businessType === 'wholesale' && v.volume_prices && v.volume_prices.length > 0) {
1005
- vVolHtml = `<div style="font-size:0.8rem; color:#636e72; margin-top:2px;">Опт: ` + v.volume_prices.map(vp => `от ${vp.qty} шт - ${vp.price} ${currency}`).join(', ') + `</div>`;
1006
- }
1007
-
1008
- let controlsHtml = '';
1009
- if (v.is_available === false) {
1010
- controlsHtml = `<span style="color:var(--danger); font-weight:bold; font-size:0.85rem;">Нет в наличии</span>`;
1011
- } else {
1012
- let addBoxBtnVariant = '';
1013
- if (businessType !== 'retail' && vPpb > 1) {
1014
- addBoxBtnVariant = `<button class="box-btn" style="height:32px; margin-right:5px;" onclick="updateCart('${p.product_id}', ${vPpb}, null, false, '${cKey}', ${moq})">+ Упаковка</button>`;
1015
- }
1016
- controlsHtml = `
1017
- ${addBoxBtnVariant}
1018
- <div class="quantity-control" style="border:none; background:var(--surface);">
1019
- <button onclick="updateCart('${p.product_id}', -1, null, false, '${cKey}', ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1020
- <input type="number" id="qty-${cKey}" value="${qty}" onchange="manualUpdateCart('${cKey}', this.value, ${moq})">
1021
- <button onclick="updateCart('${p.product_id}', 1, null, false, '${cKey}', ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1022
- </div>
1023
- `;
1024
  }
1025
 
1026
  variantsHtml += `
1027
- <div class="variant-item ${vClass}">
1028
  <div class="variant-info">
1029
  <span class="variant-name">${v.name}</span>
1030
  <span class="variant-price">${priceText}</span>
1031
- ${vVolHtml}
1032
  ${vStockHtml}
1033
  </div>
1034
  <div style="display:flex; align-items:center;">
1035
- ${controlsHtml}
 
 
 
 
 
1036
  </div>
1037
  </div>
1038
  `;
@@ -1042,58 +1011,36 @@ CATALOG_TEMPLATE = '''
1042
  let mStockHtml = showStock && p.stock !== "" && p.stock !== null ? `<div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">Остаток: ${p.stock} шт</div>` : '';
1043
  let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
1044
 
 
 
 
 
 
1045
  let priceText = `${p.price} ${currency}`;
1046
  if (businessType === 'mixed' && p.box_price && ppb > 1) {
1047
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${p.box_price} ${currency}</span>`;
1048
  }
1049
-
1050
- let volHtml = '';
1051
- if (businessType === 'wholesale' && p.volume_prices && p.volume_prices.length > 0) {
1052
- volHtml = `<div style="font-size:0.8rem; color:#636e72; margin-top:4px;">Опт: ` + p.volume_prices.map(vp => `от ${vp.qty} шт - ${vp.price} ${currency}`).join(', ') + `</div>`;
1053
- }
1054
 
1055
- if (p.is_available === false) {
1056
- mainControlsHtml = `
1057
- <div class="product-bottom">
1058
- <div style="display:flex; flex-direction:column;">
1059
- <div class="product-price">${priceText}</div>
1060
- ${volHtml}
1061
- ${mStockHtml}
1062
- </div>
1063
- <div class="controls-wrapper">
1064
- <span style="color:var(--danger); font-weight:bold;">Нет в наличии</span>
1065
- </div>
1066
  </div>
1067
- `;
1068
- } else {
1069
- let addBoxBtn = '';
1070
- if (businessType !== 'retail' && ppb > 1) {
1071
- addBoxBtn = `<button class="box-btn" onclick="updateCart('${p.product_id}', ${ppb}, null, false, null, ${moq})">+ Упаковка</button>`;
1072
- }
1073
- mainControlsHtml = `
1074
- <div class="product-bottom">
1075
- <div style="display:flex; flex-direction:column;">
1076
- <div class="product-price">${priceText}</div>
1077
- ${volHtml}
1078
- ${mStockHtml}
1079
- </div>
1080
- <div class="controls-wrapper">
1081
- ${addBoxBtn}
1082
- <div class="quantity-control">
1083
- <button onclick="updateCart('${p.product_id}', -1, null, false, null, ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1084
- <input type="number" id="qty-${p.product_id}" value="${qty}" onchange="manualUpdateCart('${p.product_id}', this.value, ${moq})">
1085
- <button onclick="updateCart('${p.product_id}', 1, null, false, null, ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1086
- </div>
1087
  </div>
1088
  </div>
1089
- `;
1090
- }
1091
  }
1092
 
1093
- let pClass = p.is_available === false && (!p.variants || p.variants.length === 0) ? 'unavailable-item' : '';
1094
-
1095
  const div = document.createElement('div');
1096
- div.className = `product-card ${pClass}`;
1097
  div.innerHTML = `
1098
  <div class="product-main-content">
1099
  <div class="product-img-wrapper" ${imgClick}>
@@ -1140,9 +1087,6 @@ CATALOG_TEMPLATE = '''
1140
  if (cKey.includes('___')) {
1141
  varIdx = parseInt(cKey.split('___')[1]);
1142
  }
1143
-
1144
- if (varIdx !== -1 && p.variants[varIdx] && p.variants[varIdx].is_available === false) return;
1145
- if (varIdx === -1 && p.is_available === false) return;
1146
 
1147
  let pStock = "";
1148
  let pPpb = parseInt(p.pieces_per_box) || 1;
@@ -1160,16 +1104,14 @@ CATALOG_TEMPLATE = '''
1160
  let price = p.price;
1161
  let bPrice = p.box_price || 0;
1162
  let vName = "";
1163
- let volPrices = p.volume_prices || [];
1164
  if (varIdx !== -1 && p.variants[varIdx]) {
1165
  if (p.has_variant_prices) {
1166
  price = p.variants[varIdx].price;
1167
  bPrice = p.variants[varIdx].box_price || 0;
1168
  }
1169
  vName = p.variants[varIdx].name;
1170
- volPrices = p.variants[varIdx].volume_prices || [];
1171
  }
1172
- cart[cKey] = { ...p, quantity: 0, cart_price: price, cart_box_price: bPrice, pieces_per_box: pPpb, variant_name: vName, variant_idx: varIdx, discount: 0, volume_prices: volPrices };
1173
  }
1174
 
1175
  let currentQty = cart[cKey].quantity;
@@ -1226,14 +1168,6 @@ CATALOG_TEMPLATE = '''
1226
  const pId = cKey.split('___')[0];
1227
  updateCart(pId, 0, num, true, cKey, moq);
1228
  }
1229
-
1230
- function getApplicablePrice(basePrice, volumePrices, qty) {
1231
- if (!volumePrices || volumePrices.length === 0) return basePrice;
1232
- for (let i = 0; i < volumePrices.length; i++) {
1233
- if (qty >= volumePrices[i].qty) return volumePrices[i].price;
1234
- }
1235
- return basePrice;
1236
- }
1237
 
1238
  function calculateItemPrice(item) {
1239
  let ppb = parseInt(item.pieces_per_box) || 1;
@@ -1241,12 +1175,9 @@ CATALOG_TEMPLATE = '''
1241
  let cBoxPrice = parseFloat(item.cart_box_price) || 0;
1242
  let cPrice = parseFloat(item.cart_price) || 0;
1243
  let disc = parseFloat(item.discount) || 0;
1244
- let volPrices = item.volume_prices || [];
1245
 
1246
  let unit = cPrice;
1247
- if (businessType === 'wholesale' && volPrices.length > 0) {
1248
- unit = getApplicablePrice(cPrice, volPrices, qty);
1249
- } else if ((businessType === 'mixed' || businessType === 'wholesale') && cBoxPrice > 0 && ppb > 1 && qty >= ppb) {
1250
  unit = cBoxPrice / ppb;
1251
  }
1252
  return Math.max(0, unit - disc) * qty;
@@ -2274,7 +2205,7 @@ ADMIN_TEMPLATE = '''
2274
 
2275
  .form-group { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 150px; }
2276
  .form-group label { font-size: 0.85rem; font-weight: 600; color: #636e72; }
2277
- .form-row { display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end; }
2278
 
2279
  .file-input-wrapper { position: relative; width: 100%; }
2280
  input[type="file"] { width: 100%; padding: 10px; border: 1px dashed #ccc; border-radius: 10px; background: #fafafa; font-size: 0.9rem; }
@@ -2288,11 +2219,9 @@ ADMIN_TEMPLATE = '''
2288
  .social-item label { display: flex; align-items: center; gap: 5px; width: 150px; cursor: pointer; }
2289
 
2290
  .variants-container { background: #f4f6f9; padding: 15px; border-radius: 10px; border: 1px dashed var(--border); display: flex; flex-direction: column; gap: 10px; }
2291
- .variant-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid var(--border); position: relative; }
2292
  .variant-row .form-group { flex: 1 1 30%; min-width: 120px; }
2293
- .remove-variant-btn { color: var(--danger); background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 5px; position: absolute; top: 10px; right: 10px; }
2294
- .move-variant-btns { position: absolute; top: 10px; right: 40px; display: flex; gap: 5px; }
2295
- .move-variant-btns button { background: none; border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-size: 0.8rem; padding: 2px 5px; color: #636e72; }
2296
 
2297
  .order-item { background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
2298
  .order-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 10px; }
@@ -2301,18 +2230,15 @@ ADMIN_TEMPLATE = '''
2301
  .staff-item { display: flex; flex-direction: column; gap: 10px; background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
2302
 
2303
  .badge { background: var(--danger); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 5px; }
2304
- .vol-price-row { display: flex; gap: 5px; align-items: center; margin-top: 5px; }
2305
- .vol-price-row input { padding: 6px; border: 1px solid var(--border); border-radius: 4px; font-size: 0.85rem; width: 80px; }
2306
- .vol-price-row button { background: var(--danger); color: white; border: none; border-radius: 4px; cursor: pointer; padding: 6px 10px; font-size: 0.8rem; }
2307
- .unavailable-item { opacity: 0.5; filter: grayscale(1); }
2308
 
2309
  @media (max-width: 600px) {
2310
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
2311
  .product-item { flex-direction: column; align-items: stretch; }
2312
  .product-info { width: 100%; }
2313
  .product-actions { align-self: flex-end; }
2314
- .form-row { flex-direction: column; gap: 10px; align-items: stretch; }
2315
- .variant-row { flex-direction: column; align-items: stretch; padding-top: 35px; }
 
2316
  }
2317
  </style>
2318
  </head>
@@ -2601,15 +2527,11 @@ ADMIN_TEMPLATE = '''
2601
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
2602
  <span class="cat-title-text"><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
2603
  </div>
2604
- <div style="display:flex; gap:5px; align-items:center;">
2605
- <form method="POST" style="margin:0;" onclick="event.stopPropagation();"><input type="hidden" name="action" value="move_category_up"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="btn btn-outline" style="padding:2px 6px; font-size:0.8rem;">▲</button></form>
2606
- <form method="POST" style="margin:0;" onclick="event.stopPropagation();"><input type="hidden" name="action" value="move_category_down"><input type="hidden" name="category_name" value="{{ category }}"><button type="submit" class="btn btn-outline" style="padding:2px 6px; font-size:0.8rem;">▼</button></form>
2607
- <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
2608
- <input type="hidden" name="action" value="delete_category">
2609
- <input type="hidden" name="category_name" value="{{ category }}">
2610
- <button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
2611
- </form>
2612
- </div>
2613
  </div>
2614
  <div class="category-content" id="cat-{{ loop.index }}">
2615
 
@@ -2650,11 +2572,6 @@ ADMIN_TEMPLATE = '''
2650
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2651
  </div>
2652
 
2653
- <div class="form-group" style="flex:0;">
2654
- <label>&nbsp;</label>
2655
- <label style="display:flex; align-items:center; gap:5px; margin-top:10px; cursor:pointer;"><input type="checkbox" name="main_is_available" value="1" checked> В наличии</label>
2656
- </div>
2657
-
2658
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2659
  <div class="form-group main-barcode-container">
2660
  <label>Штрих-код</label>
@@ -2699,14 +2616,6 @@ ADMIN_TEMPLATE = '''
2699
  {% endif %}
2700
  </div>
2701
 
2702
- {% if settings.business_type == 'wholesale' %}
2703
- <div class="form-group main-vol-prices-container" style="background: #f9f9f9; padding: 10px; border-radius: 8px; border: 1px dashed #ccc; margin-top: 10px;">
2704
- <label style="font-size:0.85rem; font-weight:bold; margin-bottom:5px; display:block;">Оптовые цены от количества (только без вариантов)</label>
2705
- <div id="main_vol_prices_add_{{ loop.index }}"></div>
2706
- <button type="button" class="btn btn-outline" style="padding:5px 10px; font-size:0.8rem; margin-top:5px;" onclick="addVolPriceRow('main_vol_prices_add_{{ loop.index }}', 'main')"><i class="fas fa-plus"></i> Добавить вариант цены</button>
2707
- </div>
2708
- {% endif %}
2709
-
2710
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
2711
  <div style="display:flex; justify-content:space-between; align-items:center;">
2712
  <label style="font-weight:600; font-size:0.9rem;"><i class="fas fa-tags"></i> Варианты товара</label>
@@ -2731,7 +2640,7 @@ ADMIN_TEMPLATE = '''
2731
 
2732
  {% for product in products %}
2733
  {% if product.category == category %}
2734
- <div class="product-item {% if product.is_available == False %}unavailable-item{% endif %}" data-pid="{{ product.product_id }}">
2735
  <div class="product-info">
2736
  {% if product.photos and product.photos|length > 0 %}
2737
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
@@ -2767,16 +2676,10 @@ ADMIN_TEMPLATE = '''
2767
  • Остаток по вариантам
2768
  {% endif %}
2769
  {% endif %}
2770
-
2771
- {% if product.is_available == False %}
2772
- <span style="color:var(--danger); font-weight:bold; margin-left:5px;">(Нет в наличии)</span>
2773
- {% endif %}
2774
  </span>
2775
  </div>
2776
  </div>
2777
- <div class="product-actions" style="align-items:center;">
2778
- <form method="POST" style="margin:0;"><input type="hidden" name="action" value="move_product_up"><input type="hidden" name="product_id" value="{{ product.product_id }}"><button type="submit" class="btn btn-outline" style="padding:4px 8px; font-size:0.8rem;">▲</button></form>
2779
- <form method="POST" style="margin:0; margin-right:10px;"><input type="hidden" name="action" value="move_product_down"><input type="hidden" name="product_id" value="{{ product.product_id }}"><button type="submit" class="btn btn-outline" style="padding:4px 8px; font-size:0.8rem;">▼</button></form>
2780
  <button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
2781
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
2782
  <input type="hidden" name="action" value="delete_product">
@@ -2798,11 +2701,6 @@ ADMIN_TEMPLATE = '''
2798
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2799
  </div>
2800
 
2801
- <div class="form-group" style="flex:0;">
2802
- <label>&nbsp;</label>
2803
- <label style="display:flex; align-items:center; gap:5px; margin-top:10px; cursor:pointer;"><input type="checkbox" name="main_is_available" value="1" {% if product.is_available != False %}checked{% endif %}> В наличии</label>
2804
- </div>
2805
-
2806
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2807
  <div class="form-group main-barcode-container" {% if product.variants %}style="display:none;"{% endif %}>
2808
  <label>Штрих-код</label>
@@ -2846,22 +2744,6 @@ ADMIN_TEMPLATE = '''
2846
  </div>
2847
  {% endif %}
2848
  </div>
2849
-
2850
- {% if settings.business_type == 'wholesale' %}
2851
- <div class="form-group main-vol-prices-container" {% if product.variants %}style="display:none;"{% else %}style="background: #f9f9f9; padding: 10px; border-radius: 8px; border: 1px dashed #ccc; margin-top: 10px;"{% endif %}>
2852
- <label style="font-size:0.85rem; font-weight:bold; margin-bottom:5px; display:block;">Оптовые цены от количества (только без вариантов)</label>
2853
- <div id="main_vol_prices_edit_{{ product.product_id }}">
2854
- {% for vp in product.volume_prices %}
2855
- <div class="vol-price-row">
2856
- <input type="number" name="main_vol_qty[]" value="{{ vp.qty }}" placeholder="От шт">
2857
- <input type="number" name="main_vol_price[]" value="{{ vp.price }}" placeholder="Цена" step="0.01">
2858
- <button type="button" onclick="this.parentElement.remove()">X</button>
2859
- </div>
2860
- {% endfor %}
2861
- </div>
2862
- <button type="button" class="btn btn-outline" style="padding:5px 10px; font-size:0.8rem; margin-top:5px;" onclick="addVolPriceRow('main_vol_prices_edit_{{ product.product_id }}', 'main')"><i class="fas fa-plus"></i> Добавить вариант цены</button>
2863
- </div>
2864
- {% endif %}
2865
 
2866
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
2867
  <div style="display:flex; justify-content:space-between; align-items:center;">
@@ -2870,71 +2752,43 @@ ADMIN_TEMPLATE = '''
2870
  </div>
2871
  <div id="variants-list-edit-{{ product.product_id }}">
2872
  {% for variant in product.variants %}
2873
- {% set vid = uuid4().hex %}
2874
  <div class="variant-row">
2875
- <div class="move-variant-btns">
2876
- <button type="button" onclick="moveVariantUp(this)"></button>
2877
- <button type="button" onclick="moveVariantDown(this)">▼</button>
2878
  </div>
2879
- <button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('edit-prod-{{ product.product_id }}')"><i class="fas fa-times-circle"></i></button>
2880
- <input type="hidden" name="variant_id[]" value="{{ vid }}">
2881
-
2882
- <div style="display:flex; width:100%; gap:15px; flex-wrap:wrap; margin-top:15px;">
2883
- <div class="form-group" style="flex:2;">
2884
- <label>Название</label>
2885
- <input type="text" name="var_name_{{ vid }}" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2886
- </div>
2887
- <div class="form-group" style="flex:0;">
2888
- <label>&nbsp;</label>
2889
- <label style="display:flex; align-items:center; gap:5px; margin-top:10px; cursor:pointer;"><input type="checkbox" name="var_is_available_{{ vid }}" value="1" {% if variant.is_available != False %}checked{% endif %}> В наличии</label>
2890
- </div>
2891
- {% if settings.business_type != 'retail' %}
2892
- <div class="form-group">
2893
- <label>В уп. (шт)</label>
2894
- <input type="number" name="var_ppb_{{ vid }}" value="{{ variant.pieces_per_box }}" placeholder="Шт">
2895
- </div>
2896
- {% endif %}
2897
- {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
2898
- <div class="form-group">
2899
- <label>Штрих-код</label>
2900
- <div style="display:flex; gap:5px;">
2901
- <input type="text" name="var_barcode_{{ vid }}" value="{{ variant.barcode }}" placeholder="Код">
2902
- <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2903
- </div>
2904
- </div>
2905
- {% endif %}
2906
- <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2907
- <label>Цена</label>
2908
- <input type="number" name="var_price_{{ vid }}" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
2909
- </div>
2910
- {% if settings.business_type == 'mixed' %}
2911
- <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2912
- <label>Цена уп.</label>
2913
- <input type="number" name="var_box_price_{{ vid }}" value="{{ variant.box_price }}" placeholder="Опционально" step="0.01">
2914
- </div>
2915
- {% endif %}
2916
- {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
2917
- <div class="form-group">
2918
- <label>Остаток</label>
2919
- <input type="number" name="var_stock_{{ vid }}" value="{{ variant.stock }}" placeholder="Остаток">
2920
- </div>
2921
- {% endif %}
2922
  </div>
2923
- {% if settings.business_type == 'wholesale' %}
2924
- <div style="width:100%; border-top:1px dashed #ccc; margin-top:10px; padding-top:10px;">
2925
- <label style="font-size:0.8rem; font-weight:bold;">Оптовые цены от количества:</label>
2926
- <div id="var_vol_prices_{{ vid }}">
2927
- {% for vp in variant.volume_prices %}
2928
- <div class="vol-price-row">
2929
- <input type="number" name="var_vol_qty_{{ vid }}[]" value="{{ vp.qty }}" placeholder="От шт">
2930
- <input type="number" name="var_vol_price_{{ vid }}[]" value="{{ vp.price }}" placeholder="Цена" step="0.01">
2931
- <button type="button" onclick="this.parentElement.remove()">X</button>
2932
- </div>
2933
- {% endfor %}
2934
  </div>
2935
- <button type="button" class="btn btn-outline" style="padding:2px 8px; font-size:0.75rem; margin-top:5px;" onclick="addVolPriceRow('var_vol_prices_{{ vid }}', 'var_{{ vid }}')">+ Добавить цену</button>
2936
  </div>
2937
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2938
  </div>
2939
  {% endfor %}
2940
  </div>
@@ -3035,40 +2889,12 @@ ADMIN_TEMPLATE = '''
3035
  document.getElementById(elId).style.display = 'none';
3036
  }
3037
 
3038
- function moveVariantUp(btn) {
3039
- const row = btn.closest('.variant-row');
3040
- if (row.previousElementSibling && row.previousElementSibling.classList.contains('variant-row')) {
3041
- row.parentNode.insertBefore(row, row.previousElementSibling);
3042
- }
3043
- }
3044
-
3045
- function moveVariantDown(btn) {
3046
- const row = btn.closest('.variant-row');
3047
- if (row.nextElementSibling && row.nextElementSibling.classList.contains('variant-row')) {
3048
- row.parentNode.insertBefore(row.nextElementSibling, row);
3049
- }
3050
- }
3051
-
3052
- function addVolPriceRow(containerId, prefix) {
3053
- const container = document.getElementById(containerId);
3054
- const div = document.createElement('div');
3055
- div.className = 'vol-price-row';
3056
- let nameQty = prefix === 'main' ? 'main_vol_qty[]' : `var_vol_qty_${prefix.replace('var_', '')}[]`;
3057
- let namePrice = prefix === 'main' ? 'main_vol_price[]' : `var_vol_price_${prefix.replace('var_', '')}[]`;
3058
- div.innerHTML = `<input type="number" name="${nameQty}" placeholder="От шт">
3059
- <input type="number" name="${namePrice}" placeholder="Цена" step="0.01">
3060
- <button type="button" onclick="this.parentElement.remove()">X</button>`;
3061
- container.appendChild(div);
3062
- }
3063
-
3064
  function addVariantRow(containerId) {
3065
  const container = document.getElementById(containerId);
3066
  const formBlock = container.closest('form');
3067
  const formId = formBlock.parentElement.id;
3068
  const hasVariantPrices = formBlock.querySelector('input[name="has_variant_prices"]').checked;
3069
 
3070
- const vid = Math.random().toString(36).substr(2, 9);
3071
-
3072
  const div = document.createElement('div');
3073
  div.className = 'variant-row';
3074
 
@@ -3076,28 +2902,17 @@ ADMIN_TEMPLATE = '''
3076
  let reqAttr = hasVariantPrices ? 'required' : '';
3077
 
3078
  let html = `
3079
- <div class="move-variant-btns">
3080
- <button type="button" onclick="moveVariantUp(this)"></button>
3081
- <button type="button" onclick="moveVariantDown(this)">▼</button>
3082
  </div>
3083
- <button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('${formId}')"><i class="fas fa-times-circle"></i></button>
3084
- <input type="hidden" name="variant_id[]" value="${vid}">
3085
- <div style="display:flex; width:100%; gap:15px; flex-wrap:wrap; margin-top:15px;">
3086
- <div class="form-group" style="flex:2;">
3087
- <label>Название</label>
3088
- <input type="text" name="var_name_${vid}" placeholder="Цвет, размер" required>
3089
- </div>
3090
- <div class="form-group" style="flex:0;">
3091
- <label>&nbsp;</label>
3092
- <label style="display:flex; align-items:center; gap:5px; margin-top:10px; cursor:pointer;"><input type="checkbox" name="var_is_available_${vid}" value="1" checked> В наличии</label>
3093
- </div>
3094
  `;
3095
 
3096
  if (businessType !== 'retail') {
3097
  html += `
3098
  <div class="form-group">
3099
  <label>В уп. (шт)</label>
3100
- <input type="number" name="var_ppb_${vid}" placeholder="Шт">
3101
  </div>`;
3102
  }
3103
 
@@ -3106,7 +2921,7 @@ ADMIN_TEMPLATE = '''
3106
  <div class="form-group">
3107
  <label>Штрих-код</label>
3108
  <div style="display:flex; gap:5px;">
3109
- <input type="text" name="var_barcode_${vid}" placeholder="Код">
3110
  <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
3111
  </div>
3112
  </div>`;
@@ -3115,7 +2930,7 @@ ADMIN_TEMPLATE = '''
3115
  html += `
3116
  <div class="form-group var-price-input" ${displayStyle}>
3117
  <label>Цена</label>
3118
- <input type="number" name="var_price_${vid}" placeholder="Цена за ед." step="0.01" ${reqAttr}>
3119
  </div>
3120
  `;
3121
 
@@ -3123,7 +2938,7 @@ ADMIN_TEMPLATE = '''
3123
  html += `
3124
  <div class="form-group var-price-input" ${displayStyle}>
3125
  <label>Цена уп.</label>
3126
- <input type="number" name="var_box_price_${vid}" placeholder="Опционально" step="0.01">
3127
  </div>`;
3128
  }
3129
 
@@ -3131,21 +2946,11 @@ ADMIN_TEMPLATE = '''
3131
  html += `
3132
  <div class="form-group">
3133
  <label>Остаток</label>
3134
- <input type="number" name="var_stock_${vid}" placeholder="Остаток">
3135
  </div>`;
3136
  }
3137
 
3138
- html += `</div>`;
3139
-
3140
- if (businessType === 'wholesale') {
3141
- html += `
3142
- <div style="width:100%; border-top:1px dashed #ccc; margin-top:10px; padding-top:10px;">
3143
- <label style="font-size:0.8rem; font-weight:bold;">Оптовые цены от количества:</label>
3144
- <div id="var_vol_prices_${vid}"></div>
3145
- <button type="button" class="btn btn-outline" style="padding:2px 8px; font-size:0.75rem; margin-top:5px;" onclick="addVolPriceRow('var_vol_prices_${vid}', 'var_${vid}')">+ Добавить цену</button>
3146
- </div>
3147
- `;
3148
- }
3149
 
3150
  div.innerHTML = html;
3151
  container.appendChild(div);
@@ -3166,8 +2971,8 @@ ADMIN_TEMPLATE = '''
3166
 
3167
  varPriceInputs.forEach(input => {
3168
  input.style.display = 'flex';
3169
- const inp = input.querySelector('input[type="number"]');
3170
- if(inp && inp.name && inp.name.includes('var_price_')) inp.setAttribute('required', 'required');
3171
  });
3172
  } else {
3173
  if(mainPriceContainer) mainPriceContainer.style.display = 'flex';
@@ -3176,8 +2981,8 @@ ADMIN_TEMPLATE = '''
3176
 
3177
  varPriceInputs.forEach(input => {
3178
  input.style.display = 'none';
3179
- const inp = input.querySelector('input[type="number"]');
3180
- if(inp && inp.name && inp.name.includes('var_price_')) inp.removeAttribute('required');
3181
  });
3182
  }
3183
  }
@@ -3201,14 +3006,6 @@ ADMIN_TEMPLATE = '''
3201
  else mainBc.style.display = 'flex';
3202
  }
3203
  }
3204
-
3205
- if(businessType === 'wholesale') {
3206
- const mainVol = form.querySelector('.main-vol-prices-container');
3207
- if(mainVol) {
3208
- if(variants.length > 0) mainVol.style.display = 'none';
3209
- else mainVol.style.display = 'block';
3210
- }
3211
- }
3212
  }
3213
 
3214
  function filterAdmin() {
@@ -3634,8 +3431,8 @@ REPORTS_TEMPLATE = '''
3634
  <tr>
3635
  <td style="font-weight:500;">${d}</td>
3636
  <td>${data[d].orders}</td>
3637
-
3638
- </tr>
3639
  `;
3640
  });
3641
  }
@@ -4208,7 +4005,6 @@ def create_order(env_id):
4208
  "variant_name": item.get('variant_name', ''),
4209
  "variant_idx": item.get('variant_idx', -1),
4210
  "discount": float(item.get('discount', 0)),
4211
- "volume_prices": item.get('volume_prices', []),
4212
  "category": product_dict.get(item.get('product_id'), 'Без категории'),
4213
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+"
4214
  })
@@ -4588,24 +4384,6 @@ def admin(env_id):
4588
  data['settings'] = settings
4589
  save_env_data(env_id, data)
4590
 
4591
- elif action == 'move_category_up':
4592
- cat_name = request.form.get('category_name')
4593
- if cat_name in categories:
4594
- idx = categories.index(cat_name)
4595
- if idx > 0:
4596
- categories[idx], categories[idx-1] = categories[idx-1], categories[idx]
4597
- data['categories'] = categories
4598
- save_env_data(env_id, data)
4599
-
4600
- elif action == 'move_category_down':
4601
- cat_name = request.form.get('category_name')
4602
- if cat_name in categories:
4603
- idx = categories.index(cat_name)
4604
- if idx < len(categories) - 1:
4605
- categories[idx], categories[idx+1] = categories[idx+1], categories[idx]
4606
- data['categories'] = categories
4607
- save_env_data(env_id, data)
4608
-
4609
  elif action == 'add_category':
4610
  cat_name = request.form.get('category_name', '').strip()
4611
  if cat_name and cat_name not in categories:
@@ -4655,30 +4433,6 @@ def admin(env_id):
4655
  del data['category_photos'][cat_name]
4656
  save_env_data(env_id, data)
4657
 
4658
- elif action == 'move_product_up':
4659
- pid = request.form.get('product_id')
4660
- for i, p in enumerate(products):
4661
- if p.get('product_id') == pid:
4662
- for j in range(i-1, -1, -1):
4663
- if products[j].get('category') == p.get('category'):
4664
- products[i], products[j] = products[j], products[i]
4665
- break
4666
- break
4667
- data['products'] = products
4668
- save_env_data(env_id, data)
4669
-
4670
- elif action == 'move_product_down':
4671
- pid = request.form.get('product_id')
4672
- for i, p in enumerate(products):
4673
- if p.get('product_id') == pid:
4674
- for j in range(i+1, len(products)):
4675
- if products[j].get('category') == p.get('category'):
4676
- products[i], products[j] = products[j], products[i]
4677
- break
4678
- break
4679
- data['products'] = products
4680
- save_env_data(env_id, data)
4681
-
4682
  elif action == 'add_product':
4683
  name = request.form.get('name', '').strip()
4684
  barcode = request.form.get('barcode', '').strip()
@@ -4697,59 +4451,48 @@ def admin(env_id):
4697
  stock_str = request.form.get('stock', '')
4698
  main_stock = int(stock_str) if stock_str else ""
4699
 
4700
- main_is_available = request.form.get('main_is_available') == '1'
4701
-
4702
  description = request.form.get('description', '').strip()
4703
  category = request.form.get('category')
4704
  has_variant_prices = 'has_variant_prices' in request.form
4705
 
4706
- main_vol_qtys = request.form.getlist('main_vol_qty[]')
4707
- main_vol_prices = request.form.getlist('main_vol_price[]')
4708
- main_volume_prices = []
4709
- for mq, mp in zip(main_vol_qtys, main_vol_prices):
4710
- if mq.strip() and mp.strip():
4711
- main_volume_prices.append({"qty": int(mq), "price": float(mp)})
 
4712
 
4713
- variant_ids = request.form.getlist('variant_id[]')
4714
- variants = []
4715
- for vid in variant_ids:
4716
- v_name = request.form.get(f'var_name_{vid}', '').strip()
4717
  if v_name:
4718
- v_is_avail = request.form.get(f'var_is_available_{vid}') == '1'
4719
  v_price = price
4720
  v_box_price = box_price
4721
  if has_variant_prices:
4722
- v_p_str = request.form.get(f'var_price_{vid}', '')
4723
- if v_p_str: v_price = float(v_p_str)
4724
- v_bp_str = request.form.get(f'var_box_price_{vid}', '')
4725
- if v_bp_str: v_box_price = float(v_bp_str)
4726
-
4727
  v_stock = ""
4728
- v_s_str = request.form.get(f'var_stock_{vid}', '')
4729
- if v_s_str: v_stock = int(v_s_str)
4730
 
4731
- v_barcode = request.form.get(f'var_barcode_{vid}', '').strip()
 
 
4732
 
4733
  v_ppb = pieces_per_box
4734
- v_ppb_str = request.form.get(f'var_ppb_{vid}', '')
4735
- if v_ppb_str: v_ppb = int(v_ppb_str)
4736
-
4737
- v_vol_qtys = request.form.getlist(f'var_vol_qty_{vid}[]')
4738
- v_vol_prices = request.form.getlist(f'var_vol_price_{vid}[]')
4739
- v_volume_prices = []
4740
- for vq, vp in zip(v_vol_qtys, v_vol_prices):
4741
- if vq.strip() and vp.strip():
4742
- v_volume_prices.append({"qty": int(vq), "price": float(vp)})
4743
-
4744
  variants.append({
4745
  "name": v_name,
4746
  "barcode": v_barcode,
4747
  "price": v_price,
4748
  "box_price": v_box_price,
4749
  "stock": v_stock,
4750
- "pieces_per_box": v_ppb,
4751
- "is_available": v_is_avail,
4752
- "volume_prices": v_volume_prices
4753
  })
4754
 
4755
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -4791,8 +4534,6 @@ def admin(env_id):
4791
  'box_price': box_price,
4792
  'min_order': min_order,
4793
  'stock': main_stock,
4794
- 'is_available': main_is_available,
4795
- 'volume_prices': main_volume_prices,
4796
  'description': description,
4797
  'category': category,
4798
  'photos': photos_list,
@@ -4823,60 +4564,45 @@ def admin(env_id):
4823
  stock_str = request.form.get('stock', '')
4824
  main_stock = int(stock_str) if stock_str else ""
4825
 
4826
- main_is_available = request.form.get('main_is_available') == '1'
4827
-
4828
  description = request.form.get('description', '').strip()
4829
  has_variant_prices = 'has_variant_prices' in request.form
4830
 
4831
- main_vol_qtys = request.form.getlist('main_vol_qty[]')
4832
- main_vol_prices = request.form.getlist('main_vol_price[]')
4833
- main_volume_prices = []
4834
- for mq, mp in zip(main_vol_qtys, main_vol_prices):
4835
- if mq.strip() and mp.strip():
4836
- main_volume_prices.append({"qty": int(mq), "price": float(mp)})
4837
-
4838
  remove_photos = request.form.getlist('remove_photos[]')
4839
 
4840
- variant_ids = request.form.getlist('variant_id[]')
4841
- variants = []
4842
- for vid in variant_ids:
4843
- v_name = request.form.get(f'var_name_{vid}', '').strip()
 
 
 
 
 
 
4844
  if v_name:
4845
- v_is_avail = request.form.get(f'var_is_available_{vid}') == '1'
4846
  v_price = price
4847
  v_box_price = box_price
4848
  if has_variant_prices:
4849
- v_p_str = request.form.get(f'var_price_{vid}', '')
4850
- if v_p_str: v_price = float(v_p_str)
4851
- v_bp_str = request.form.get(f'var_box_price_{vid}', '')
4852
- if v_bp_str: v_box_price = float(v_bp_str)
4853
-
4854
  v_stock = ""
4855
- v_s_str = request.form.get(f'var_stock_{vid}', '')
4856
- if v_s_str: v_stock = int(v_s_str)
4857
-
4858
- v_barcode = request.form.get(f'var_barcode_{vid}', '').strip()
4859
-
4860
  v_ppb = pieces_per_box
4861
- v_ppb_str = request.form.get(f'var_ppb_{vid}', '')
4862
- if v_ppb_str: v_ppb = int(v_ppb_str)
4863
-
4864
- v_vol_qtys = request.form.getlist(f'var_vol_qty_{vid}[]')
4865
- v_vol_prices = request.form.getlist(f'var_vol_price_{vid}[]')
4866
- v_volume_prices = []
4867
- for vq, vp in zip(v_vol_qtys, v_vol_prices):
4868
- if vq.strip() and vp.strip():
4869
- v_volume_prices.append({"qty": int(vq), "price": float(vp)})
4870
-
4871
  variants.append({
4872
  "name": v_name,
4873
  "barcode": v_barcode,
4874
  "price": v_price,
4875
  "box_price": v_box_price,
4876
  "stock": v_stock,
4877
- "pieces_per_box": v_ppb,
4878
- "is_available": v_is_avail,
4879
- "volume_prices": v_volume_prices
4880
  })
4881
 
4882
  uploaded_photos = request.files.getlist('photos')[:10]
@@ -4918,8 +4644,6 @@ def admin(env_id):
4918
  p['box_price'] = box_price
4919
  p['min_order'] = min_order
4920
  p['stock'] = main_stock
4921
- p['is_available'] = main_is_available
4922
- p['volume_prices'] = main_volume_prices
4923
  p['description'] = description
4924
  p['variants'] = variants
4925
  p['has_variant_prices'] = has_variant_prices
 
235
  if 'variants' not in product: product['variants'] =[]; changed = True
236
  if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True
237
  if 'stock' not in product: product['stock'] = ""; changed = True
 
 
238
  for v in product['variants']:
239
  if 'stock' not in v: v['stock'] = ""; changed = True
240
  if 'box_price' not in v: v['box_price'] = ""; changed = True
241
  if 'barcode' not in v: v['barcode'] = ""; changed = True
242
  if 'pieces_per_box' not in v: v['pieces_per_box'] = product.get('pieces_per_box', ""); changed = True
 
 
243
 
244
  for order_id, order in env_data['orders'].items():
245
  if 'status' not in order: order['status'] = 'confirmed'; changed = True
 
249
  for item in order.get('cart', []):
250
  if 'discount' not in item: item['discount'] = 0; changed = True
251
  if 'category' not in item: item['category'] = 'Без категории'; changed = True
 
252
 
253
  if changed or not os.path.exists(DATA_FILE):
254
  try:
 
313
  all_data[env_id] = env_data
314
  save_data(all_data)
315
 
 
 
 
 
 
 
 
 
316
  def update_order_totals(order, business_type):
317
  total = 0
318
  global_discount = float(order.get('global_discount', 0))
 
324
  c_price = float(i.get('price', 0))
325
  c_box_price = float(i.get('cart_box_price', 0))
326
  item_discount = float(i.get('discount', 0))
 
327
 
328
+ if business_type in ['mixed', 'wholesale'] and c_box_price > 0 and ppb > 1 and qty >= ppb:
329
+ base_price = c_box_price / ppb
330
+ else:
331
+ base_price = c_price
 
332
 
333
+ discounted_price = max(0, base_price - item_discount)
334
  item_total = discounted_price * qty
335
  i['calculated_price'] = round(discounted_price, 2)
336
  total += item_total
 
677
  .process-return-btn { background: #e17055; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; }
678
 
679
  .history-btn { background: #0984e3; color: white; border: none; padding: 8px 15px; border-radius: 8px; cursor: pointer; margin-top: 10px; width: 100%; font-weight: 600; text-decoration: none; display: block; text-align: center; box-sizing: border-box; }
 
680
 
681
  @media (min-width: 768px) {
682
  .categories-container { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
 
971
  if (p.variants && p.variants.length > 0) {
972
  variantsHtml = `<div class="variants-list">`;
973
  p.variants.forEach((v, idx) => {
 
974
  let vPrice = p.has_variant_prices ? v.price : p.price;
975
  let vBoxPrice = p.has_variant_prices ? (v.box_price || '') : (p.box_price || '');
976
  let vStockHtml = showStock && v.stock !== "" && v.stock !== null ? `<div class="variant-stock">Остаток: ${v.stock} шт</div>` : '';
 
983
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${vBoxPrice} ${currency}</span>`;
984
  }
985
 
986
+ let addBoxBtnVariant = '';
987
+ if (businessType !== 'retail' && vPpb > 1) {
988
+ addBoxBtnVariant = `<button class="box-btn" style="height:32px; margin-right:5px;" onclick="updateCart('${p.product_id}', ${vPpb}, null, false, '${cKey}', ${moq})">+ Упаковка</button>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
989
  }
990
 
991
  variantsHtml += `
992
+ <div class="variant-item">
993
  <div class="variant-info">
994
  <span class="variant-name">${v.name}</span>
995
  <span class="variant-price">${priceText}</span>
 
996
  ${vStockHtml}
997
  </div>
998
  <div style="display:flex; align-items:center;">
999
+ ${addBoxBtnVariant}
1000
+ <div class="quantity-control" style="border:none; background:var(--surface);">
1001
+ <button onclick="updateCart('${p.product_id}', -1, null, false, '${cKey}', ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1002
+ <input type="number" id="qty-${cKey}" value="${qty}" onchange="manualUpdateCart('${cKey}', this.value, ${moq})">
1003
+ <button onclick="updateCart('${p.product_id}', 1, null, false, '${cKey}', ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
1004
+ </div>
1005
  </div>
1006
  </div>
1007
  `;
 
1011
  let mStockHtml = showStock && p.stock !== "" && p.stock !== null ? `<div style="font-size:0.8rem; color:#0984e3; margin-top:4px;">Остаток: ${p.stock} шт</div>` : '';
1012
  let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
1013
 
1014
+ let addBoxBtn = '';
1015
+ if (businessType !== 'retail' && ppb > 1) {
1016
+ addBoxBtn = `<button class="box-btn" onclick="updateCart('${p.product_id}', ${ppb}, null, false, null, ${moq})">+ Упаковка</button>`;
1017
+ }
1018
+
1019
  let priceText = `${p.price} ${currency}`;
1020
  if (businessType === 'mixed' && p.box_price && ppb > 1) {
1021
  priceText += `<br><span style="font-size:0.8rem; color:#636e72;">Упаковка: ${p.box_price} ${currency}</span>`;
1022
  }
 
 
 
 
 
1023
 
1024
+ mainControlsHtml = `
1025
+ <div class="product-bottom">
1026
+ <div style="display:flex; flex-direction:column;">
1027
+ <div class="product-price">${priceText}</div>
1028
+ ${mStockHtml}
 
 
 
 
 
 
1029
  </div>
1030
+ <div class="controls-wrapper">
1031
+ ${addBoxBtn}
1032
+ <div class="quantity-control">
1033
+ <button onclick="updateCart('${p.product_id}', -1, null, false, null, ${moq})"><i class="fas fa-minus" style="font-size:0.8rem;"></i></button>
1034
+ <input type="number" id="qty-${p.product_id}" value="${qty}" onchange="manualUpdateCart('${p.product_id}', this.value, ${moq})">
1035
+ <button onclick="updateCart('${p.product_id}', 1, null, false, null, ${moq})"><i class="fas fa-plus" style="font-size:0.8rem;"></i></button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  </div>
1037
  </div>
1038
+ </div>
1039
+ `;
1040
  }
1041
 
 
 
1042
  const div = document.createElement('div');
1043
+ div.className = 'product-card';
1044
  div.innerHTML = `
1045
  <div class="product-main-content">
1046
  <div class="product-img-wrapper" ${imgClick}>
 
1087
  if (cKey.includes('___')) {
1088
  varIdx = parseInt(cKey.split('___')[1]);
1089
  }
 
 
 
1090
 
1091
  let pStock = "";
1092
  let pPpb = parseInt(p.pieces_per_box) || 1;
 
1104
  let price = p.price;
1105
  let bPrice = p.box_price || 0;
1106
  let vName = "";
 
1107
  if (varIdx !== -1 && p.variants[varIdx]) {
1108
  if (p.has_variant_prices) {
1109
  price = p.variants[varIdx].price;
1110
  bPrice = p.variants[varIdx].box_price || 0;
1111
  }
1112
  vName = p.variants[varIdx].name;
 
1113
  }
1114
+ cart[cKey] = { ...p, quantity: 0, cart_price: price, cart_box_price: bPrice, pieces_per_box: pPpb, variant_name: vName, variant_idx: varIdx, discount: 0 };
1115
  }
1116
 
1117
  let currentQty = cart[cKey].quantity;
 
1168
  const pId = cKey.split('___')[0];
1169
  updateCart(pId, 0, num, true, cKey, moq);
1170
  }
 
 
 
 
 
 
 
 
1171
 
1172
  function calculateItemPrice(item) {
1173
  let ppb = parseInt(item.pieces_per_box) || 1;
 
1175
  let cBoxPrice = parseFloat(item.cart_box_price) || 0;
1176
  let cPrice = parseFloat(item.cart_price) || 0;
1177
  let disc = parseFloat(item.discount) || 0;
 
1178
 
1179
  let unit = cPrice;
1180
+ if ((businessType === 'mixed' || businessType === 'wholesale') && cBoxPrice > 0 && ppb > 1 && qty >= ppb) {
 
 
1181
  unit = cBoxPrice / ppb;
1182
  }
1183
  return Math.max(0, unit - disc) * qty;
 
2205
 
2206
  .form-group { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 150px; }
2207
  .form-group label { font-size: 0.85rem; font-weight: 600; color: #636e72; }
2208
+ .form-row { display: flex; gap: 15px; flex-wrap: wrap; }
2209
 
2210
  .file-input-wrapper { position: relative; width: 100%; }
2211
  input[type="file"] { width: 100%; padding: 10px; border: 1px dashed #ccc; border-radius: 10px; background: #fafafa; font-size: 0.9rem; }
 
2219
  .social-item label { display: flex; align-items: center; gap: 5px; width: 150px; cursor: pointer; }
2220
 
2221
  .variants-container { background: #f4f6f9; padding: 15px; border-radius: 10px; border: 1px dashed var(--border); display: flex; flex-direction: column; gap: 10px; }
2222
+ .variant-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid var(--border); }
2223
  .variant-row .form-group { flex: 1 1 30%; min-width: 120px; }
2224
+ .remove-variant-btn { color: var(--danger); background: none; border: none; font-size: 1.2rem; cursor: pointer; padding: 12px 5px; flex: 0 0 auto; }
 
 
2225
 
2226
  .order-item { background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
2227
  .order-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 10px; }
 
2230
  .staff-item { display: flex; flex-direction: column; gap: 10px; background: #fff; padding: 15px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 10px; }
2231
 
2232
  .badge { background: var(--danger); color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 5px; }
 
 
 
 
2233
 
2234
  @media (max-width: 600px) {
2235
  .header-panel { flex-direction: column; align-items: stretch; text-align: center; }
2236
  .product-item { flex-direction: column; align-items: stretch; }
2237
  .product-info { width: 100%; }
2238
  .product-actions { align-self: flex-end; }
2239
+ .form-row { flex-direction: column; gap: 10px; }
2240
+ .variant-row { flex-direction: column; align-items: stretch; }
2241
+ .remove-variant-btn { width: 100%; text-align: right; padding: 5px; }
2242
  }
2243
  </style>
2244
  </head>
 
2527
  <i class="fas fa-chevron-down" id="icon-cat-{{ loop.index }}" style="color: #636e72;"></i>
2528
  <span class="cat-title-text"><i class="fas fa-folder-open" style="color:var(--info); margin-right:5px;"></i> {{ category }}</span>
2529
  </div>
2530
+ <form method="POST" style="margin:0;" onclick="event.stopPropagation();" onsubmit="return confirm('Удалить категорию и все ее товары?');">
2531
+ <input type="hidden" name="action" value="delete_category">
2532
+ <input type="hidden" name="category_name" value="{{ category }}">
2533
+ <button type="submit" class="btn btn-danger"><i class="fas fa-trash-alt"></i></button>
2534
+ </form>
 
 
 
 
2535
  </div>
2536
  <div class="category-content" id="cat-{{ loop.index }}">
2537
 
 
2572
  <input type="text" name="name" placeholder="Введите название" required autocomplete="off">
2573
  </div>
2574
 
 
 
 
 
 
2575
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2576
  <div class="form-group main-barcode-container">
2577
  <label>Штрих-код</label>
 
2616
  {% endif %}
2617
  </div>
2618
 
 
 
 
 
 
 
 
 
2619
  <div class="variants-container" id="variants-container-add-{{ loop.index }}">
2620
  <div style="display:flex; justify-content:space-between; align-items:center;">
2621
  <label style="font-weight:600; font-size:0.9rem;"><i class="fas fa-tags"></i> Варианты товара</label>
 
2640
 
2641
  {% for product in products %}
2642
  {% if product.category == category %}
2643
+ <div class="product-item" data-pid="{{ product.product_id }}">
2644
  <div class="product-info">
2645
  {% if product.photos and product.photos|length > 0 %}
2646
  <img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product.photos[0] }}" class="product-img">
 
2676
  • Остаток по вариантам
2677
  {% endif %}
2678
  {% endif %}
 
 
 
 
2679
  </span>
2680
  </div>
2681
  </div>
2682
+ <div class="product-actions">
 
 
2683
  <button class="btn btn-warning" onclick="toggleEditProduct('edit-prod-{{ product.product_id }}')"><i class="fas fa-edit"></i></button>
2684
  <form method="POST" style="margin:0;" onsubmit="return confirm('Удалить товар?');">
2685
  <input type="hidden" name="action" value="delete_product">
 
2701
  <input type="text" name="name" value="{{ product.name }}" required autocomplete="off">
2702
  </div>
2703
 
 
 
 
 
 
2704
  {% if settings.use_barcodes and sys_mode not in ['external', 'light_external'] %}
2705
  <div class="form-group main-barcode-container" {% if product.variants %}style="display:none;"{% endif %}>
2706
  <label>Штрих-код</label>
 
2744
  </div>
2745
  {% endif %}
2746
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2747
 
2748
  <div class="variants-container" id="variants-container-edit-{{ product.product_id }}">
2749
  <div style="display:flex; justify-content:space-between; align-items:center;">
 
2752
  </div>
2753
  <div id="variants-list-edit-{{ product.product_id }}">
2754
  {% for variant in product.variants %}
 
2755
  <div class="variant-row">
2756
+ <div class="form-group">
2757
+ <label>Название</label>
2758
+ <input type="text" name="variant_name[]" value="{{ variant.name }}" placeholder="Цвет, размер" required>
2759
  </div>
2760
+ {% if settings.business_type != 'retail' %}
2761
+ <div class="form-group">
2762
+ <label>В уп. (шт)</label>
2763
+ <input type="number" name="variant_pieces_per_box[]" value="{{ variant.pieces_per_box }}" placeholder="Шт">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2764
  </div>
2765
+ {% endif %}
2766
+ {% if settings.use_barcodes and sys_mode not in['external', 'light_external'] %}
2767
+ <div class="form-group">
2768
+ <label>Штрих-код</label>
2769
+ <div style="display:flex; gap:5px;">
2770
+ <input type="text" name="variant_barcode[]" value="{{ variant.barcode }}" placeholder="Код">
2771
+ <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
 
 
 
 
2772
  </div>
 
2773
  </div>
2774
  {% endif %}
2775
+ <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2776
+ <label>Цена</label>
2777
+ <input type="number" name="variant_price[]" value="{{ variant.price }}" placeholder="Цена за ед." step="0.01" {% if product.has_variant_prices %}required{% endif %}>
2778
+ </div>
2779
+ {% if settings.business_type == 'mixed' %}
2780
+ <div class="form-group var-price-input" {% if not product.has_variant_prices %}style="display:none;"{% endif %}>
2781
+ <label>Цена уп.</label>
2782
+ <input type="number" name="variant_box_price[]" value="{{ variant.box_price }}" placeholder="Опционально" step="0.01">
2783
+ </div>
2784
+ {% endif %}
2785
+ {% if settings.track_inventory and sys_mode not in ['external', 'light_external'] %}
2786
+ <div class="form-group">
2787
+ <label>Остаток</label>
2788
+ <input type="number" name="variant_stock[]" value="{{ variant.stock }}" placeholder="Остаток">
2789
+ </div>
2790
+ {% endif %}
2791
+ <button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('edit-prod-{{ product.product_id }}')"><i class="fas fa-times-circle"></i></button>
2792
  </div>
2793
  {% endfor %}
2794
  </div>
 
2889
  document.getElementById(elId).style.display = 'none';
2890
  }
2891
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2892
  function addVariantRow(containerId) {
2893
  const container = document.getElementById(containerId);
2894
  const formBlock = container.closest('form');
2895
  const formId = formBlock.parentElement.id;
2896
  const hasVariantPrices = formBlock.querySelector('input[name="has_variant_prices"]').checked;
2897
 
 
 
2898
  const div = document.createElement('div');
2899
  div.className = 'variant-row';
2900
 
 
2902
  let reqAttr = hasVariantPrices ? 'required' : '';
2903
 
2904
  let html = `
2905
+ <div class="form-group">
2906
+ <label>Название</label>
2907
+ <input type="text" name="variant_name[]" placeholder="Цвет, размер" required>
2908
  </div>
 
 
 
 
 
 
 
 
 
 
 
2909
  `;
2910
 
2911
  if (businessType !== 'retail') {
2912
  html += `
2913
  <div class="form-group">
2914
  <label>В уп. (шт)</label>
2915
+ <input type="number" name="variant_pieces_per_box[]" placeholder="Шт">
2916
  </div>`;
2917
  }
2918
 
 
2921
  <div class="form-group">
2922
  <label>Штрих-код</label>
2923
  <div style="display:flex; gap:5px;">
2924
+ <input type="text" name="variant_barcode[]" placeholder="Код">
2925
  <button type="button" class="btn btn-outline" style="padding: 12px;" onclick="startScanner(val => this.previousElementSibling.value = val)"><i class="fas fa-barcode"></i></button>
2926
  </div>
2927
  </div>`;
 
2930
  html += `
2931
  <div class="form-group var-price-input" ${displayStyle}>
2932
  <label>Цена</label>
2933
+ <input type="number" name="variant_price[]" placeholder="Цена за ед." step="0.01" ${reqAttr}>
2934
  </div>
2935
  `;
2936
 
 
2938
  html += `
2939
  <div class="form-group var-price-input" ${displayStyle}>
2940
  <label>Цена уп.</label>
2941
+ <input type="number" name="variant_box_price[]" placeholder="Опционально" step="0.01">
2942
  </div>`;
2943
  }
2944
 
 
2946
  html += `
2947
  <div class="form-group">
2948
  <label>Остаток</label>
2949
+ <input type="number" name="variant_stock[]" placeholder="Остаток">
2950
  </div>`;
2951
  }
2952
 
2953
+ html += `<button type="button" class="remove-variant-btn" onclick="this.parentElement.remove(); updateMainStockVisibility('${formId}')"><i class="fas fa-times-circle"></i></button>`;
 
 
 
 
 
 
 
 
 
 
2954
 
2955
  div.innerHTML = html;
2956
  container.appendChild(div);
 
2971
 
2972
  varPriceInputs.forEach(input => {
2973
  input.style.display = 'flex';
2974
+ const inp = input.querySelector('input[name="variant_price[]"]');
2975
+ if(inp) inp.setAttribute('required', 'required');
2976
  });
2977
  } else {
2978
  if(mainPriceContainer) mainPriceContainer.style.display = 'flex';
 
2981
 
2982
  varPriceInputs.forEach(input => {
2983
  input.style.display = 'none';
2984
+ const inp = input.querySelector('input[name="variant_price[]"]');
2985
+ if(inp) inp.removeAttribute('required');
2986
  });
2987
  }
2988
  }
 
3006
  else mainBc.style.display = 'flex';
3007
  }
3008
  }
 
 
 
 
 
 
 
 
3009
  }
3010
 
3011
  function filterAdmin() {
 
3431
  <tr>
3432
  <td style="font-weight:500;">${d}</td>
3433
  <td>${data[d].orders}</td>
3434
+ <td>${Math.round(data[d].sum).toLocaleString()}</td>
3435
+ </tr>
3436
  `;
3437
  });
3438
  }
 
4005
  "variant_name": item.get('variant_name', ''),
4006
  "variant_idx": item.get('variant_idx', -1),
4007
  "discount": float(item.get('discount', 0)),
 
4008
  "category": product_dict.get(item.get('product_id'), 'Без категории'),
4009
  "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photos'][0]}" if item.get('photos') else "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMHcwIi8+PC9zdmc+"
4010
  })
 
4384
  data['settings'] = settings
4385
  save_env_data(env_id, data)
4386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4387
  elif action == 'add_category':
4388
  cat_name = request.form.get('category_name', '').strip()
4389
  if cat_name and cat_name not in categories:
 
4433
  del data['category_photos'][cat_name]
4434
  save_env_data(env_id, data)
4435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4436
  elif action == 'add_product':
4437
  name = request.form.get('name', '').strip()
4438
  barcode = request.form.get('barcode', '').strip()
 
4451
  stock_str = request.form.get('stock', '')
4452
  main_stock = int(stock_str) if stock_str else ""
4453
 
 
 
4454
  description = request.form.get('description', '').strip()
4455
  category = request.form.get('category')
4456
  has_variant_prices = 'has_variant_prices' in request.form
4457
 
4458
+ variant_names = request.form.getlist('variant_name[]')
4459
+ variant_barcodes = request.form.getlist('variant_barcode[]')
4460
+ variant_prices = request.form.getlist('variant_price[]')
4461
+ variant_box_prices = request.form.getlist('variant_box_price[]')
4462
+ variant_stocks = request.form.getlist('variant_stock[]')
4463
+ variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4464
+ variants =[]
4465
 
4466
+ for i in range(len(variant_names)):
4467
+ v_name = variant_names[i].strip()
 
 
4468
  if v_name:
 
4469
  v_price = price
4470
  v_box_price = box_price
4471
  if has_variant_prices:
4472
+ if i < len(variant_prices) and variant_prices[i]:
4473
+ v_price = float(variant_prices[i])
4474
+ if i < len(variant_box_prices) and variant_box_prices[i]:
4475
+ v_box_price = float(variant_box_prices[i])
4476
+
4477
  v_stock = ""
4478
+ if i < len(variant_stocks) and variant_stocks[i]:
4479
+ v_stock = int(variant_stocks[i])
4480
 
4481
+ v_barcode = ""
4482
+ if i < len(variant_barcodes) and variant_barcodes[i]:
4483
+ v_barcode = variant_barcodes[i].strip()
4484
 
4485
  v_ppb = pieces_per_box
4486
+ if i < len(variant_ppbs) and variant_ppbs[i]:
4487
+ v_ppb = int(variant_ppbs[i])
4488
+
 
 
 
 
 
 
 
4489
  variants.append({
4490
  "name": v_name,
4491
  "barcode": v_barcode,
4492
  "price": v_price,
4493
  "box_price": v_box_price,
4494
  "stock": v_stock,
4495
+ "pieces_per_box": v_ppb
 
 
4496
  })
4497
 
4498
  uploaded_photos = request.files.getlist('photos')[:10]
 
4534
  'box_price': box_price,
4535
  'min_order': min_order,
4536
  'stock': main_stock,
 
 
4537
  'description': description,
4538
  'category': category,
4539
  'photos': photos_list,
 
4564
  stock_str = request.form.get('stock', '')
4565
  main_stock = int(stock_str) if stock_str else ""
4566
 
 
 
4567
  description = request.form.get('description', '').strip()
4568
  has_variant_prices = 'has_variant_prices' in request.form
4569
 
 
 
 
 
 
 
 
4570
  remove_photos = request.form.getlist('remove_photos[]')
4571
 
4572
+ variant_names = request.form.getlist('variant_name[]')
4573
+ variant_barcodes = request.form.getlist('variant_barcode[]')
4574
+ variant_prices = request.form.getlist('variant_price[]')
4575
+ variant_box_prices = request.form.getlist('variant_box_price[]')
4576
+ variant_stocks = request.form.getlist('variant_stock[]')
4577
+ variant_ppbs = request.form.getlist('variant_pieces_per_box[]')
4578
+ variants =[]
4579
+
4580
+ for i in range(len(variant_names)):
4581
+ v_name = variant_names[i].strip()
4582
  if v_name:
 
4583
  v_price = price
4584
  v_box_price = box_price
4585
  if has_variant_prices:
4586
+ if i < len(variant_prices) and variant_prices[i]:
4587
+ v_price = float(variant_prices[i])
4588
+ if i < len(variant_box_prices) and variant_box_prices[i]:
4589
+ v_box_price = float(variant_box_prices[i])
 
4590
  v_stock = ""
4591
+ if i < len(variant_stocks) and variant_stocks[i]:
4592
+ v_stock = int(variant_stocks[i])
4593
+ v_barcode = ""
4594
+ if i < len(variant_barcodes) and variant_barcodes[i]:
4595
+ v_barcode = variant_barcodes[i].strip()
4596
  v_ppb = pieces_per_box
4597
+ if i < len(variant_ppbs) and variant_ppbs[i]:
4598
+ v_ppb = int(variant_ppbs[i])
 
 
 
 
 
 
 
 
4599
  variants.append({
4600
  "name": v_name,
4601
  "barcode": v_barcode,
4602
  "price": v_price,
4603
  "box_price": v_box_price,
4604
  "stock": v_stock,
4605
+ "pieces_per_box": v_ppb
 
 
4606
  })
4607
 
4608
  uploaded_photos = request.files.getlist('photos')[:10]
 
4644
  p['box_price'] = box_price
4645
  p['min_order'] = min_order
4646
  p['stock'] = main_stock
 
 
4647
  p['description'] = description
4648
  p['variants'] = variants
4649
  p['has_variant_prices'] = has_variant_prices