diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -125,33 +125,6 @@ def load_data():
else:
data = {}
- if 'products' in data or 'categories' in data:
- data = {
- 'default_env': {
- 'products': data.get('products', []),
- 'categories': data.get('categories', []),
- 'orders': data.get('orders', {}),
- 'staff': [],
- 'settings': {
- 'organization_name': 'Default Shop',
- 'admin_password_enabled': False,
- 'admin_password': '',
- 'logo_url': DEFAULT_LOGO_URL,
- 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
- 'track_inventory': False,
- 'business_type': 'retail',
- 'customer_fields': {
- 'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False
- },
- 'socials': {
- 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'},
- 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'},
- 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'}
- }
- }
- }
- }
-
changed = False
for env_id, env_data in data.items():
if 'products' not in env_data: env_data['products'] = []
@@ -163,44 +136,55 @@ def load_data():
changed = True
settings = env_data['settings']
- if 'organization_name' not in settings: settings['organization_name'] = f'Shop {env_id}'; changed = True
- if 'admin_password_enabled' not in settings: settings['admin_password_enabled'] = False; changed = True
- if 'admin_password' not in settings: settings['admin_password'] = ''; changed = True
- if 'logo_url' not in settings: settings['logo_url'] = DEFAULT_LOGO_URL; changed = True
- if 'whatsapp_number' not in settings: settings['whatsapp_number'] = DEFAULT_WHATSAPP_NUMBER; changed = True
- if 'track_inventory' not in settings: settings['track_inventory'] = False; changed = True
- if 'business_type' not in settings: settings['business_type'] = 'retail'; changed = True
- if 'customer_fields' not in settings:
- settings['customer_fields'] = {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False}
- changed = True
- if 'socials' not in settings:
- settings['socials'] = {
- 'wa': {'enabled': True, 'url': 'https://wa.me/77011333885'},
- 'ig': {'enabled': True, 'url': 'https://instagram.com/14sklad_baisat'},
- 'tg': {'enabled': True, 'url': 'https://t.me/posuda15konteiner'}
+ defaults = {
+ 'organization_name': f'Shop {env_id}',
+ 'admin_password_enabled': False,
+ 'admin_password': '',
+ 'logo_url': DEFAULT_LOGO_URL,
+ 'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
+ 'track_inventory': False,
+ 'business_type': 'оптово-розничный',
+ 'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
+ 'socials': {
+ 'wa': {'enabled': True, 'url': ''},
+ 'ig': {'enabled': True, 'url': ''},
+ 'tg': {'enabled': True, 'url': ''}
}
- changed = True
+ }
+ for key, value in defaults.items():
+ if key not in settings:
+ settings[key] = value
+ changed = True
for product in env_data['products']:
- if 'product_id' not in product: product['product_id'] = uuid4().hex; changed = True
- if 'pieces_per_box' not in product: product['pieces_per_box'] = 1; changed = True
- if 'min_qty' not in product: product['min_qty'] = 1; changed = True
- if 'box_price' not in product: product['box_price'] = 0; changed = True
- if 'variants' not in product: product['variants'] = []; changed = True
- if 'has_variant_prices' not in product: product['has_variant_prices'] = False; changed = True
- if 'stock' not in product: product['stock'] = 0; changed = True
- for v in product['variants']:
- if 'stock' not in v: v['stock'] = 0; changed = True
- if 'box_price' not in v: v['box_price'] = 0; changed = True
+ p_defaults = {
+ 'product_id': uuid4().hex,
+ 'pieces_per_box': 1,
+ 'variants': [],
+ 'has_variant_prices': False,
+ 'stock': 0,
+ 'min_order_qty': 1
+ }
+ for key, value in p_defaults.items():
+ if key not in product:
+ product[key] = value
+ changed = True
+ for v in product.get('variants', []):
+ if 'stock' not in v:
+ v['stock'] = 0
+ changed = True
for order_id, order in env_data['orders'].items():
- if 'status' not in order: order['status'] = 'confirmed'; changed = True
- if 'staff_name' not in order: order['staff_name'] = ''; changed = True
+ o_defaults = {'status': 'confirmed', 'staff_name': ''}
+ for key, value in o_defaults.items():
+ if key not in order:
+ order[key] = value
+ changed = True
if changed or not os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
- json.dump(data, f)
+ json.dump(data, f, ensure_ascii=False, indent=4)
except Exception:
pass
@@ -231,7 +215,7 @@ def get_env_data(env_id):
'logo_url': DEFAULT_LOGO_URL,
'whatsapp_number': DEFAULT_WHATSAPP_NUMBER,
'track_inventory': False,
- 'business_type': 'retail',
+ 'business_type': 'оптово-розничный',
'customer_fields': {'name': True, 'phone': True, 'city': True, 'address': False, 'zip': False},
'socials': {
'wa': {'enabled': True, 'url': ''},
@@ -435,7 +419,7 @@ CATALOG_TEMPLATE = '''
.product-title { font-size: 0.95rem; font-weight: 600; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.product-desc { font-size: 0.8rem; color: var(--text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.product-box-info { font-size: 0.8rem; color: #00b894; font-weight: 600; }
- .product-wholesale-info { font-size: 0.8rem; color: #e17055; font-weight: 600; }
+ .product-min-order { font-size: 0.8rem; color: #e17055; font-weight: 600; }
.product-bottom { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-top: 5px; flex-wrap: wrap; gap: 10px; }
.product-price { font-weight: 700; font-size: 1rem; color: var(--primary); }
@@ -586,8 +570,8 @@ CATALOG_TEMPLATE = '''
{% if mode == 'pos' %}
-
-
+
+
{% else %}
{% if settings.customer_fields.name %}
{% endif %}
{% if settings.customer_fields.phone %}
{% endif %}
@@ -632,8 +616,8 @@ CATALOG_TEMPLATE = '''
const mode = '{{ mode }}';
const staffId = '{{ staff_id }}';
const trackInventory = {{ 'true' if settings.track_inventory else 'false' }};
+ const businessType = '{{ settings.business_type }}';
const cFields = {{ settings.customer_fields|tojson }};
- const bType = '{{ settings.business_type }}';
let cart = {};
let currentGalleryPhotos = [];
@@ -707,25 +691,8 @@ CATALOG_TEMPLATE = '''
}
}
- function formatQtyText(qty, ppb) {
- if (bType === 'retail') return `${qty} шт.`;
- ppb = parseInt(ppb) || 1;
- if (ppb > 1 && qty >= ppb) {
- let boxes = Math.floor(qty / ppb);
- let remainder = qty % ppb;
- return `${boxes} кор.` + (remainder > 0 ? ` ${remainder} шт.` : '');
- }
- return `${qty} шт.`;
- }
-
function renderProductCard(p, container) {
- const isRetail = bType === 'retail';
- const isWholesaleRetail = bType === 'wholesale_retail';
- const isWholesale = bType === 'wholesale';
-
const ppb = parseInt(p.pieces_per_box) || 1;
- const minQty = parseInt(p.min_qty) || 1;
-
const hasPhotos = p.photos && p.photos.length > 0;
const photoUrl = hasPhotos
? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${p.photos[0]}`
@@ -734,12 +701,13 @@ CATALOG_TEMPLATE = '''
const photoIndicator = hasPhotos && p.photos.length > 1 ? `
${p.photos.length}
` : '';
const imgClick = hasPhotos ? `onclick="openGallery('${p.product_id}')"` : '';
const descHtml = p.description ? `
${p.description}
` : '';
-
let boxInfoHtml = '';
- if (isWholesaleRetail && ppb > 1) {
+ if (businessType !== 'розница' && ppb > 1) {
boxInfoHtml = `
В коробке: ${ppb} шт
`;
- } else if (isWholesale && minQty > 1) {
- boxInfoHtml = `
Мин. заказ: ${minQty} шт
`;
+ }
+ let minOrderHtml = '';
+ if (businessType === 'оптовый' && p.min_order_qty > 1) {
+ minOrderHtml = `
Мин. заказ: ${p.min_order_qty} шт
`;
}
let variantsHtml = '';
@@ -749,33 +717,25 @@ CATALOG_TEMPLATE = '''
variantsHtml = `
`;
p.variants.forEach((v, idx) => {
let vPrice = p.has_variant_prices ? v.price : p.price;
- let vBoxPrice = v.box_price || (vPrice * ppb);
-
- let priceDisplay = `${vPrice} ${currency}`;
- if (isWholesaleRetail && ppb > 1) {
- priceDisplay = `${vPrice} ${currency}/шт
Коробка: ${vBoxPrice} ${currency}`;
+ let boxPriceHtml = '';
+ if (businessType === 'оптово-розничный' && ppb > 1) {
+ boxPriceHtml = ` / ${(vPrice * ppb).toFixed(0)} ${currency} за коробку`;
}
-
let vStockHtml = trackInventory ? `
Остаток: ${v.stock || 0} шт
` : '';
let cKey = getCartKey(p.product_id, idx);
let qty = cart[cKey] ? cart[cKey].quantity : 0;
- let addBoxBtn = (isWholesaleRetail && ppb > 1) ? `
` : '';
-
variantsHtml += `
${v.name}
- ${priceDisplay}
+ ${vPrice} ${currency}${boxPriceHtml}
${vStockHtml}
-
`;
@@ -784,19 +744,17 @@ CATALOG_TEMPLATE = '''
} else {
let mStockHtml = trackInventory ? `
Остаток: ${p.stock || 0} шт
` : '';
let qty = cart[p.product_id] ? cart[p.product_id].quantity : 0;
-
- let bPrice = p.box_price || (p.price * ppb);
- let priceDisplay = `${p.price} ${currency}`;
- if (isWholesaleRetail && ppb > 1) {
- priceDisplay = `${p.price} ${currency}/шт
Коробка: ${bPrice} ${currency}`;
+ let addBoxBtn = '';
+ let boxPriceText = '';
+ if (businessType === 'оптово-розничный' && ppb > 1) {
+ addBoxBtn = `
`;
+ boxPriceText = ` / ${(p.price * ppb).toFixed(0)} ${currency} за коробку`;
}
-
- let addBoxBtn = (isWholesaleRetail && ppb > 1) ? `
` : '';
mainControlsHtml = `
-
${priceDisplay}
+
${p.price} ${currency}${boxPriceText}
${mStockHtml}
@@ -823,6 +781,7 @@ CATALOG_TEMPLATE = '''
${p.name}
${descHtml}
${boxInfoHtml}
+ ${minOrderHtml}
${variantsHtml}
@@ -859,36 +818,27 @@ CATALOG_TEMPLATE = '''
varIdx = parseInt(cKey.split('___')[1]);
}
- let currentQty = cart[cKey] ? cart[cKey].quantity : 0;
- let newQty = exactValue !== null ? exactValue : currentQty + change;
-
- if (bType === 'wholesale') {
- let minQ = parseInt(p.min_qty) || 1;
- if (change > 0 && currentQty === 0 && newQty < minQ) {
- newQty = minQ;
- } else if (change < 0 && newQty > 0 && newQty < minQ) {
- newQty = 0;
- }
- }
-
- if (!cart[cKey] && newQty > 0) {
+ if (!cart[cKey]) {
let price = p.price;
- let bPrice = p.box_price || (price * (p.pieces_per_box || 1));
let vName = "";
if (varIdx !== -1 && p.variants[varIdx]) {
if (p.has_variant_prices) price = p.variants[varIdx].price;
- bPrice = p.variants[varIdx].box_price || (price * (p.pieces_per_box || 1));
vName = p.variants[varIdx].name;
}
- cart[cKey] = { ...p, quantity: 0, cart_price: price, box_price: bPrice, variant_name: vName, variant_idx: varIdx };
+ cart[cKey] = { ...p, quantity: 0, cart_price: price, variant_name: vName, variant_idx: varIdx };
+ }
+
+ if (exactValue !== null) {
+ cart[cKey].quantity = exactValue;
+ } else {
+ cart[cKey].quantity += change;
}
- if (newQty <= 0) {
+ if (cart[cKey].quantity <= 0) {
delete cart[cKey];
const input = document.getElementById(`qty-${cKey}`);
if(input) input.value = 0;
} else {
- cart[cKey].quantity = newQty;
const input = document.getElementById(`qty-${cKey}`);
if(input) input.value = cart[cKey].quantity;
}
@@ -913,14 +863,7 @@ CATALOG_TEMPLATE = '''
function updateCartUI() {
let total = 0;
for (let cKey in cart) {
- const item = cart[cKey];
- if (bType === 'wholesale_retail' && item.pieces_per_box > 1) {
- let boxes = Math.floor(item.quantity / item.pieces_per_box);
- let units = item.quantity % item.pieces_per_box;
- total += (boxes * item.box_price) + (units * item.cart_price);
- } else {
- total += item.cart_price * item.quantity;
- }
+ total += cart[cKey].cart_price * cart[cKey].quantity;
}
const cartBar = document.getElementById('cartBar');
@@ -943,8 +886,6 @@ CATALOG_TEMPLATE = '''
for (let cKey in cart) {
const item = cart[cKey];
- const ppb = parseInt(item.pieces_per_box) || 1;
- const formattedQty = formatQtyText(item.quantity, ppb);
const pId = item.product_id;
let nameDisplay = item.name;
@@ -952,20 +893,10 @@ CATALOG_TEMPLATE = '''
nameDisplay += `
(${item.variant_name})
`;
}
- let lineTotal = 0;
- if (bType === 'wholesale_retail' && ppb > 1) {
- let boxes = Math.floor(item.quantity / ppb);
- let units = item.quantity % ppb;
- lineTotal = (boxes * item.box_price) + (units * item.cart_price);
- } else {
- lineTotal = item.cart_price * item.quantity;
- }
-
list.innerHTML += `
${nameDisplay}
-
${formattedQty}
@@ -975,7 +906,7 @@ CATALOG_TEMPLATE = '''
-
${lineTotal} ${currency}
+
${item.cart_price * item.quantity} ${currency}
`;
}
@@ -1008,40 +939,31 @@ CATALOG_TEMPLATE = '''
let orderData = { cart: cartArray, mode: mode, staff_id: staffId };
if (mode === 'pos') {
- const nameEl = document.getElementById('custNamePos');
- if(!nameEl.value.trim()) {
- alert('Укажите имя клиента!');
- return;
- }
- orderData.customer_name = nameEl.value.trim();
+ const nameEl = document.getElementById('custName');
const waEl = document.getElementById('custWhatsapp');
+ orderData.customer_name = nameEl ? nameEl.value.trim() : '';
orderData.customer_whatsapp = waEl ? waEl.value.trim() : '';
} else {
let fail = false;
if(cFields.name) {
const el = document.getElementById('custName');
- if(!el.value.trim()) fail = true;
- orderData.customer_name = el.value.trim();
+ if(!el || !el.value.trim()) fail = true; else orderData.customer_name = el.value.trim();
}
if(cFields.phone) {
const el = document.getElementById('custPhone');
- if(!el.value.trim()) fail = true;
- orderData.customer_phone = el.value.trim();
+ if(!el || !el.value.trim()) fail = true; else orderData.customer_phone = el.value.trim();
}
if(cFields.city) {
const el = document.getElementById('custCity');
- if(!el.value.trim()) fail = true;
- orderData.customer_city = el.value.trim();
+ if(!el || !el.value.trim()) fail = true; else orderData.customer_city = el.value.trim();
}
if(cFields.address) {
const el = document.getElementById('custAddress');
- if(!el.value.trim()) fail = true;
- orderData.customer_address = el.value.trim();
+ if(!el || !el.value.trim()) fail = true; else orderData.customer_address = el.value.trim();
}
if(cFields.zip) {
const el = document.getElementById('custZip');
- if(!el.value.trim()) fail = true;
- orderData.customer_zip = el.value.trim();
+ if(!el || !el.value.trim()) fail = true; else orderData.customer_zip = el.value.trim();
}
if(fail) {
alert('Пожалуйста, заполните все обязательные поля.');
@@ -1192,29 +1114,13 @@ CATALOG_TEMPLATE = '''
}
}
- function nextPhoto(e) {
- if(e) e.stopPropagation();
- if(currentGalleryPhotos.length <= 1) return;
- currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length;
- updateGalleryView();
- }
+ function nextPhoto(e) { if(e) e.stopPropagation(); if(currentGalleryPhotos.length <= 1) return; currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryPhotos.length; updateGalleryView(); }
+ function prevPhoto(e) { if(e) e.stopPropagation(); if(currentGalleryPhotos.length <= 1) return; currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length; updateGalleryView(); }
- function prevPhoto(e) {
- if(e) e.stopPropagation();
- if(currentGalleryPhotos.length <= 1) return;
- currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryPhotos.length) % currentGalleryPhotos.length;
- updateGalleryView();
- }
-
- let touchstartX = 0;
- let touchendX = 0;
+ let touchstartX = 0, touchendX = 0;
const swipeArea = document.getElementById('gallerySwipeArea');
swipeArea.addEventListener('touchstart', e => { touchstartX = e.changedTouches[0].screenX; });
- swipeArea.addEventListener('touchend', e => {
- touchendX = e.changedTouches[0].screenX;
- if (touchstartX - touchendX > 50) nextPhoto();
- if (touchendX - touchstartX > 50) prevPhoto();
- });
+ swipeArea.addEventListener('touchend', e => { touchendX = e.changedTouches[0].screenX; if (touchstartX - touchendX > 50) nextPhoto(); if (touchendX - touchstartX > 50) prevPhoto(); });
init();
@@ -1353,13 +1259,6 @@ ORDER_TEMPLATE = '''
{% set ppb = item.pieces_per_box|default(1)|int %}
{% set boxes = item.quantity // ppb %}
{% set remainder = item.quantity % ppb %}
-
- {% set line_total = item.price * item.quantity %}
- {% if settings.business_type == 'wholesale_retail' and ppb > 1 %}
- {% set b_price = item.box_price if item.box_price else (item.price * ppb) %}
- {% set line_total = (boxes * b_price) + (remainder * item.price) %}
- {% endif %}
-
{% if item.quantity > 0 %}
| {{ loop.index }} |
@@ -1384,7 +1283,7 @@ ORDER_TEMPLATE = '''
{% endif %}
- {% if settings.business_type != 'retail' and ppb > 1 and boxes > 0 %}
+ {% if ppb > 1 and boxes > 0 %}
{{ boxes }} кор.{% if remainder > 0 %} {{ remainder }} шт.{% endif %}
{% else %}
{{ item.quantity }} шт.
@@ -1393,7 +1292,7 @@ ORDER_TEMPLATE = '''
- {% if settings.business_type != 'retail' and ppb > 1 and boxes > 0 %}
+ {% if ppb > 1 and boxes > 0 %}
{{ boxes }} кор.{% if remainder > 0 %} {{ remainder }} шт.{% endif %}
{% else %}
{{ item.quantity }} шт.
@@ -1401,7 +1300,7 @@ ORDER_TEMPLATE = '''
{{ item.price }} |
- {{ line_total }} |
+ {{ item.price * item.quantity }} |
{% endif %}
{% endfor %}
@@ -1424,7 +1323,6 @@ ORDER_TEMPLATE = '''