Update app.py
Browse files
app.py
CHANGED
|
@@ -3572,738 +3572,7 @@ def inventory_action(env_id):
|
|
| 3572 |
'id': uuid4().hex,
|
| 3573 |
'product_id': p_id,
|
| 3574 |
'tag_id': t_id,
|
| 3575 |
-
'type': action,
|
| 3576 |
-
if batch['qty'] > 0:
|
| 3577 |
-
if batch['qty'] >= remaining_to_deduct:
|
| 3578 |
-
batch['qty'] -= remaining_to_deduct
|
| 3579 |
-
remaining_to_deduct = 0
|
| 3580 |
-
break
|
| 3581 |
-
else:
|
| 3582 |
-
remaining_to_deduct -= batch['qty']
|
| 3583 |
-
batch['qty'] = 0
|
| 3584 |
-
|
| 3585 |
-
tag['stock'] -= qty
|
| 3586 |
-
history_entry['details'] = 'Ручное списание'
|
| 3587 |
-
update_tag_price_from_batches(tag)
|
| 3588 |
-
|
| 3589 |
-
else:
|
| 3590 |
-
return jsonify({"error": "Неизвестное действие"}), 400
|
| 3591 |
-
|
| 3592 |
-
if 'inventory_history' not in data:
|
| 3593 |
-
data['inventory_history'] = []
|
| 3594 |
-
data['inventory_history'].append(history_entry)
|
| 3595 |
-
|
| 3596 |
-
save_env_data(env_id, data)
|
| 3597 |
-
return jsonify({"success": True})
|
| 3598 |
-
|
| 3599 |
-
@app.route('/<env_id>/inventory_history/<p_id>/<t_id>')
|
| 3600 |
-
def inventory_history(env_id, p_id, t_id):
|
| 3601 |
-
data = get_env_data(env_id)
|
| 3602 |
-
history = data.get('inventory_history',[])
|
| 3603 |
-
item_history =[h for h in history if h.get('product_id') == p_id and h.get('tag_id') == t_id]
|
| 3604 |
-
item_history.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 3605 |
-
return jsonify(item_history)
|
| 3606 |
-
|
| 3607 |
-
@app.route('/<env_id>/reports')
|
| 3608 |
-
def reports_page(env_id):
|
| 3609 |
-
data = get_env_data(env_id)
|
| 3610 |
-
settings = data.get('settings', {})
|
| 3611 |
-
if settings.get('env_mode') != '2in1':
|
| 3612 |
-
return "Отчеты доступны только в режиме '2 в 1'", 403
|
| 3613 |
-
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
|
| 3614 |
-
return redirect(url_for('admin_login', env_id=env_id))
|
| 3615 |
-
|
| 3616 |
-
now = datetime.now(ALMATY_TZ)
|
| 3617 |
-
default_start = now.replace(day=1).strftime('%Y-%m-%d')
|
| 3618 |
-
default_end = now.strftime('%Y-%m-%d')
|
| 3619 |
-
|
| 3620 |
-
start_date = request.args.get('start_date', default_start)
|
| 3621 |
-
end_date = request.args.get('end_date', default_end)
|
| 3622 |
-
|
| 3623 |
-
orders = data.get('orders', {}).values()
|
| 3624 |
-
filtered_orders =[]
|
| 3625 |
-
|
| 3626 |
-
for o in orders:
|
| 3627 |
-
created_at = o.get('created_at', '')
|
| 3628 |
-
if created_at:
|
| 3629 |
-
date_part = created_at.split(' ')[0]
|
| 3630 |
-
if start_date <= date_part <= end_date:
|
| 3631 |
-
filtered_orders.append(o)
|
| 3632 |
-
|
| 3633 |
-
total_orders = len(filtered_orders)
|
| 3634 |
-
total_revenue = sum(o.get('total_price', 0) for o in filtered_orders)
|
| 3635 |
-
pos_orders = sum(1 for o in filtered_orders if o.get('source') == 'pos')
|
| 3636 |
-
online_orders = total_orders - pos_orders
|
| 3637 |
-
|
| 3638 |
-
emp_stats = {}
|
| 3639 |
-
product_sales = {}
|
| 3640 |
-
|
| 3641 |
-
for o in filtered_orders:
|
| 3642 |
-
emp = o.get('employee_name') or 'Прямой заказ'
|
| 3643 |
-
if emp not in emp_stats:
|
| 3644 |
-
emp_stats[emp] = {'count': 0, 'revenue': 0}
|
| 3645 |
-
emp_stats[emp]['count'] += 1
|
| 3646 |
-
emp_stats[emp]['revenue'] += o.get('total_price', 0)
|
| 3647 |
-
|
| 3648 |
-
for item in o.get('cart',[]):
|
| 3649 |
-
name = item.get('name', 'Неизвестно')
|
| 3650 |
-
qty = item.get('quantity', 0)
|
| 3651 |
-
if name not in product_sales:
|
| 3652 |
-
product_sales[name] = 0
|
| 3653 |
-
product_sales[name] += qty
|
| 3654 |
-
|
| 3655 |
-
top_products =[{'name': k, 'qty': v} for k, v in product_sales.items()]
|
| 3656 |
-
top_products.sort(key=lambda x: x['qty'], reverse=True)
|
| 3657 |
-
|
| 3658 |
-
return render_template_string(
|
| 3659 |
-
REPORTS_TEMPLATE, env_id=env_id, settings=settings, currency_code=settings.get('currency_code', 'KGS'),
|
| 3660 |
-
total_orders=total_orders, total_revenue=total_revenue, pos_orders=pos_orders, online_orders=online_orders,
|
| 3661 |
-
emp_stats=emp_stats, top_products=top_products[:20], start_date=start_date, end_date=end_date
|
| 3662 |
-
)
|
| 3663 |
-
|
| 3664 |
-
@app.route('/<env_id>/track_view/<product_id>', methods=['POST'])
|
| 3665 |
-
def track_view(env_id, product_id):
|
| 3666 |
-
data = get_env_data(env_id)
|
| 3667 |
-
for p in data['products']:
|
| 3668 |
-
if p.get('product_id') == product_id:
|
| 3669 |
-
p['views'] = p.get('views', 0) + 1
|
| 3670 |
-
break
|
| 3671 |
-
save_env_data(env_id, data)
|
| 3672 |
-
return jsonify({"status": "ok"})
|
| 3673 |
-
|
| 3674 |
-
@app.route('/<env_id>/product/<product_id>')
|
| 3675 |
-
def product_detail(env_id, product_id):
|
| 3676 |
-
data = get_env_data(env_id)
|
| 3677 |
-
all_products_raw = data.get('products',[])
|
| 3678 |
-
settings = data.get('settings', {})
|
| 3679 |
-
env_mode = settings.get('env_mode', 'external')
|
| 3680 |
-
|
| 3681 |
-
products_in_stock =[]
|
| 3682 |
-
for p in all_products_raw:
|
| 3683 |
-
if not p.get('in_stock', True):
|
| 3684 |
-
continue
|
| 3685 |
-
if env_mode == '2in1':
|
| 3686 |
-
valid_tags =[t for t in p.get('tags', []) if t.get('stock', 0) > 0]
|
| 3687 |
-
if not valid_tags and p.get('tags',[]):
|
| 3688 |
-
continue
|
| 3689 |
-
p_copy = p.copy()
|
| 3690 |
-
p_copy['tags'] = valid_tags
|
| 3691 |
-
products_in_stock.append(p_copy)
|
| 3692 |
-
else:
|
| 3693 |
-
products_in_stock.append(p)
|
| 3694 |
-
|
| 3695 |
-
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 3696 |
-
|
| 3697 |
-
product = next((p for p in products_sorted if p.get('product_id') == product_id), None)
|
| 3698 |
-
if not product:
|
| 3699 |
-
return "Товар не найден или отсутствует в наличии.", 404
|
| 3700 |
-
|
| 3701 |
-
return render_template_string(
|
| 3702 |
-
PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID,
|
| 3703 |
-
currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id
|
| 3704 |
-
)
|
| 3705 |
-
|
| 3706 |
-
@app.route('/<env_id>/create_order', methods=['POST'])
|
| 3707 |
-
def create_order(env_id):
|
| 3708 |
-
order_data = request.get_json()
|
| 3709 |
-
if not order_data or 'cart' not in order_data or not order_data['cart']:
|
| 3710 |
-
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 3711 |
-
|
| 3712 |
-
data = get_env_data(env_id)
|
| 3713 |
-
settings = data.get('settings', {})
|
| 3714 |
-
products = data.get('products',[])
|
| 3715 |
-
env_mode = settings.get('env_mode', 'external')
|
| 3716 |
-
|
| 3717 |
-
cart_items = order_data['cart']
|
| 3718 |
-
customer_data = order_data.get('customer_data', {})
|
| 3719 |
-
emp_id = order_data.get('emp_id')
|
| 3720 |
-
source = order_data.get('source', 'catalog')
|
| 3721 |
-
emp_name = None
|
| 3722 |
-
emp_whatsapp = None
|
| 3723 |
-
|
| 3724 |
-
if emp_id:
|
| 3725 |
-
employees = data.get('employees',[])
|
| 3726 |
-
for emp in employees:
|
| 3727 |
-
if emp.get('id') == emp_id:
|
| 3728 |
-
emp_name = emp.get('name')
|
| 3729 |
-
emp_whatsapp = emp.get('whatsapp')
|
| 3730 |
-
break
|
| 3731 |
-
|
| 3732 |
-
total_price = 0
|
| 3733 |
-
processed_cart =[]
|
| 3734 |
-
order_timestamp = datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S')
|
| 3735 |
-
|
| 3736 |
-
for item in cart_items:
|
| 3737 |
-
if not all(k in item for k in ('name', 'quantity')):
|
| 3738 |
-
return jsonify({"error": "Неверный формат товара в корзине."}), 400
|
| 3739 |
-
try:
|
| 3740 |
-
quantity = int(item['quantity'])
|
| 3741 |
-
if quantity <= 0:
|
| 3742 |
-
raise ValueError("Invalid quantity")
|
| 3743 |
-
|
| 3744 |
-
p_id = item.get('product_id')
|
| 3745 |
-
c_color = item.get('color', 'N/A')
|
| 3746 |
-
tx = item.get('tag_x')
|
| 3747 |
-
ty = item.get('tag_y')
|
| 3748 |
-
u_type = item.get('unit_type', 'piece')
|
| 3749 |
-
|
| 3750 |
-
product_ref = next((p for p in products if p.get('product_id') == p_id), None)
|
| 3751 |
-
if not product_ref:
|
| 3752 |
-
return jsonify({"error": f"Товар {p_id} не найден."}), 400
|
| 3753 |
-
|
| 3754 |
-
tag_ref = None
|
| 3755 |
-
if 'TAG_' in c_color:
|
| 3756 |
-
tag_id = c_color.split('_VAR_')[0].replace('TAG_', '')
|
| 3757 |
-
tag_ref = next((t for t in product_ref.get('tags',[]) if t.get('id') == tag_id), None)
|
| 3758 |
-
elif item.get('id') and len(item['id'].split('-')) >= 2:
|
| 3759 |
-
tag_id = item['id'].split('-')[1]
|
| 3760 |
-
tag_ref = next((t for t in product_ref.get('tags',[]) if t.get('id') == tag_id), None)
|
| 3761 |
-
|
| 3762 |
-
price = float(item.get('price', 0))
|
| 3763 |
-
discount_applied = False
|
| 3764 |
-
|
| 3765 |
-
if tag_ref:
|
| 3766 |
-
orig_price = float(tag_ref.get('price', 0))
|
| 3767 |
-
box_price = float(tag_ref.get('box_price', orig_price))
|
| 3768 |
-
box_qty = int(tag_ref.get('box_qty', 1))
|
| 3769 |
-
|
| 3770 |
-
if u_type == 'piece' and box_qty > 1 and quantity >= box_qty:
|
| 3771 |
-
price = box_price / box_qty
|
| 3772 |
-
discount_applied = True
|
| 3773 |
-
elif u_type == 'box':
|
| 3774 |
-
price = box_price
|
| 3775 |
-
else:
|
| 3776 |
-
price = orig_price
|
| 3777 |
-
|
| 3778 |
-
if env_mode == '2in1':
|
| 3779 |
-
deduction = quantity
|
| 3780 |
-
if u_type == 'box':
|
| 3781 |
-
deduction = quantity * box_qty
|
| 3782 |
-
|
| 3783 |
-
if tag_ref.get('stock', 0) < deduction:
|
| 3784 |
-
return jsonify({"error": f"Недостаточно остатков для товара {item['name']}."}), 400
|
| 3785 |
-
|
| 3786 |
-
if 'stock_batches' not in tag_ref:
|
| 3787 |
-
tag_ref['stock_batches'] =[{"qty": tag_ref.get('stock', 0), "price": tag_ref.get('price', 0), "box_price": tag_ref.get('box_price', 0)}]
|
| 3788 |
-
|
| 3789 |
-
remaining_to_deduct = deduction
|
| 3790 |
-
for batch in tag_ref['stock_batches']:
|
| 3791 |
-
if batch['qty'] > 0:
|
| 3792 |
-
if batch['qty'] >= remaining_to_deduct:
|
| 3793 |
-
batch['qty'] -= remaining_to_deduct
|
| 3794 |
-
remaining_to_deduct = 0
|
| 3795 |
-
break
|
| 3796 |
-
else:
|
| 3797 |
-
remaining_to_deduct -= batch['qty']
|
| 3798 |
-
batch['qty'] = 0
|
| 3799 |
-
|
| 3800 |
-
tag_ref['stock'] -= deduction
|
| 3801 |
-
update_tag_price_from_batches(tag_ref)
|
| 3802 |
-
|
| 3803 |
-
if 'inventory_history' not in data:
|
| 3804 |
-
data['inventory_history'] = []
|
| 3805 |
-
data['inventory_history'].append({
|
| 3806 |
-
'id': uuid4().hex,
|
| 3807 |
-
'product_id': p_id,
|
| 3808 |
-
'tag_id': tag_ref['id'],
|
| 3809 |
-
'type': 'sale',
|
| 3810 |
-
'qty': deduction,
|
| 3811 |
-
'timestamp': order_timestamp,
|
| 3812 |
-
'details': f"Продажа ({source})"
|
| 3813 |
-
})
|
| 3814 |
-
|
| 3815 |
-
processed_cart.append({
|
| 3816 |
-
"product_id": p_id, "name": item['name'], "price": price, "quantity": quantity,
|
| 3817 |
-
"color": c_color, "photo": item.get('photo'), "tag_x": tx, "tag_y": ty, "unit_type": u_type,
|
| 3818 |
-
"discount_applied": discount_applied,
|
| 3819 |
-
"photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A"
|
| 3820 |
-
})
|
| 3821 |
-
total_price += price * quantity
|
| 3822 |
-
except (ValueError, TypeError) as e:
|
| 3823 |
-
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
|
| 3824 |
-
|
| 3825 |
-
order_id = f"{datetime.now(ALMATY_TZ).strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
|
| 3826 |
-
|
| 3827 |
-
new_order = {
|
| 3828 |
-
"id": order_id, "created_at": order_timestamp, "cart": processed_cart,
|
| 3829 |
-
"total_price": round(total_price, 2), "status": "new",
|
| 3830 |
-
"employee_id": emp_id, "employee_name": emp_name, "employee_whatsapp": emp_whatsapp,
|
| 3831 |
-
"customer_data": customer_data, "source": source
|
| 3832 |
-
}
|
| 3833 |
-
|
| 3834 |
-
try:
|
| 3835 |
-
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 3836 |
-
data['orders'] = {}
|
| 3837 |
-
data['orders'][order_id] = new_order
|
| 3838 |
-
data['products'] = products
|
| 3839 |
-
save_env_data(env_id, data)
|
| 3840 |
-
return jsonify({"order_id": order_id}), 201
|
| 3841 |
-
except Exception:
|
| 3842 |
-
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 3843 |
-
|
| 3844 |
-
@app.route('/<env_id>/update_order/<order_id>', methods=['POST'])
|
| 3845 |
-
def update_order(env_id, order_id):
|
| 3846 |
-
data = get_env_data(env_id)
|
| 3847 |
-
order = data.get('orders', {}).get(order_id)
|
| 3848 |
-
if not order:
|
| 3849 |
-
return jsonify({"error": "Заказ не найден."}), 404
|
| 3850 |
-
|
| 3851 |
-
req = request.get_json()
|
| 3852 |
-
idx = req.get('index')
|
| 3853 |
-
action = req.get('action')
|
| 3854 |
-
|
| 3855 |
-
if idx is None or action not in['inc', 'dec', 'set', 'remove']:
|
| 3856 |
-
return jsonify({"error": "Некорректный запрос."}), 400
|
| 3857 |
-
|
| 3858 |
-
try:
|
| 3859 |
-
idx = int(idx)
|
| 3860 |
-
cart = order.get('cart',[])
|
| 3861 |
-
if idx < 0 or idx >= len(cart):
|
| 3862 |
-
return jsonify({"error": "Товар не найден."}), 404
|
| 3863 |
-
|
| 3864 |
-
if action == 'inc':
|
| 3865 |
-
cart[idx]['quantity'] += 1
|
| 3866 |
-
elif action == 'dec':
|
| 3867 |
-
cart[idx]['quantity'] -= 1
|
| 3868 |
-
if cart[idx]['quantity'] <= 0:
|
| 3869 |
-
cart.pop(idx)
|
| 3870 |
-
elif action == 'set':
|
| 3871 |
-
val = int(req.get('value', 1))
|
| 3872 |
-
if val <= 0:
|
| 3873 |
-
cart.pop(idx)
|
| 3874 |
-
else:
|
| 3875 |
-
cart[idx]['quantity'] = val
|
| 3876 |
-
elif action == 'remove':
|
| 3877 |
-
cart.pop(idx)
|
| 3878 |
-
|
| 3879 |
-
total = sum(float(item['price']) * int(item['quantity']) for item in cart)
|
| 3880 |
-
order['total_price'] = round(total, 2)
|
| 3881 |
-
order['cart'] = cart
|
| 3882 |
-
|
| 3883 |
-
save_env_data(env_id, data)
|
| 3884 |
-
return jsonify({"success": True})
|
| 3885 |
-
except Exception as e:
|
| 3886 |
-
return jsonify({"error": str(e)}), 500
|
| 3887 |
-
|
| 3888 |
-
@app.route('/<env_id>/delete_order/<order_id>', methods=['POST'])
|
| 3889 |
-
def delete_order(env_id, order_id):
|
| 3890 |
-
data = get_env_data(env_id)
|
| 3891 |
-
if 'orders' in data and order_id in data['orders']:
|
| 3892 |
-
del data['orders'][order_id]
|
| 3893 |
-
save_env_data(env_id, data)
|
| 3894 |
-
flash("Заказ успешно удален.", "success")
|
| 3895 |
-
else:
|
| 3896 |
-
flash("Заказ не найден.", "error")
|
| 3897 |
-
return redirect(url_for('history_page', env_id=env_id))
|
| 3898 |
-
|
| 3899 |
-
@app.route('/<env_id>/order/<order_id>')
|
| 3900 |
-
def view_order(env_id, order_id):
|
| 3901 |
-
data = get_env_data(env_id)
|
| 3902 |
-
order = data.get('orders', {}).get(order_id)
|
| 3903 |
-
settings = data.get('settings', {})
|
| 3904 |
-
return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id)
|
| 3905 |
-
|
| 3906 |
-
@app.route('/<env_id>/history')
|
| 3907 |
-
def history_page(env_id):
|
| 3908 |
-
data = get_env_data(env_id)
|
| 3909 |
-
settings = data.get('settings', {})
|
| 3910 |
-
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
|
| 3911 |
-
return redirect(url_for('admin_login', env_id=env_id))
|
| 3912 |
-
|
| 3913 |
-
orders = list(data.get('orders', {}).values())
|
| 3914 |
-
orders.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
| 3915 |
-
employees = data.get('employees',[])
|
| 3916 |
-
return render_template_string(HISTORY_TEMPLATE, orders=orders, employees=employees, settings=settings, currency_code=settings.get('currency_code', 'KGS'), env_id=env_id)
|
| 3917 |
-
|
| 3918 |
-
@app.route('/<env_id>/admin_ai_chat', methods=['POST'])
|
| 3919 |
-
def admin_ai_chat(env_id):
|
| 3920 |
-
data = get_env_data(env_id)
|
| 3921 |
-
settings = data.get('settings', {})
|
| 3922 |
-
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
|
| 3923 |
-
return jsonify({"text": "Доступ запрещен."})
|
| 3924 |
-
|
| 3925 |
-
if not configure_gemini():
|
| 3926 |
-
return jsonify({"text": "AI не настроен."})
|
| 3927 |
-
|
| 3928 |
-
req = request.get_json()
|
| 3929 |
-
message = req.get('message')
|
| 3930 |
-
history = req.get('history',[])
|
| 3931 |
-
|
| 3932 |
-
orders = data.get('orders', {})
|
| 3933 |
-
products = data.get('products',[])
|
| 3934 |
-
|
| 3935 |
-
now = datetime.now(ALMATY_TZ)
|
| 3936 |
-
current_month = now.strftime('%Y-%m')
|
| 3937 |
-
|
| 3938 |
-
monthly_revenue = 0
|
| 3939 |
-
product_sales_counts = {}
|
| 3940 |
-
|
| 3941 |
-
for o in orders.values():
|
| 3942 |
-
if o.get('created_at', '').startswith(current_month):
|
| 3943 |
-
monthly_revenue += o.get('total_price', 0)
|
| 3944 |
-
for item in o.get('cart',[]):
|
| 3945 |
-
pid = item.get('product_id')
|
| 3946 |
-
product_sales_counts[pid] = product_sales_counts.get(pid, 0) + item.get('quantity', 0)
|
| 3947 |
-
|
| 3948 |
-
sorted_views = sorted(products, key=lambda x: x.get('views', 0), reverse=True)[:5]
|
| 3949 |
-
views_str_list = [f"[POST: {p['product_id']} Название: {p['name']}] (просмотров: {p.get('views', 0)})" for p in sorted_views if p.get('views', 0) > 0]
|
| 3950 |
-
views_str = ", ".join(views_str_list) if views_str_list else "Нет просмотров"
|
| 3951 |
-
|
| 3952 |
-
sorted_sales_pids = sorted(product_sales_counts.items(), key=lambda x: x[1], reverse=True)[:5]
|
| 3953 |
-
sales_str_list =[]
|
| 3954 |
-
for pid, qty in sorted_sales_pids:
|
| 3955 |
-
p = next((x for x in products if x['product_id'] == pid), None)
|
| 3956 |
-
if p:
|
| 3957 |
-
sales_str_list.append(f"[POST: {pid} Название: {p['name']}] (продано: {qty} шт)")
|
| 3958 |
-
sales_str = ", ".join(sales_str_list) if sales_str_list else "Пока нет продаж"
|
| 3959 |
-
|
| 3960 |
-
currency = data['settings'].get('currency_code', 'KGS')
|
| 3961 |
-
|
| 3962 |
-
sys_prompt = f"""Ты — умный AI-ассистент администратора магазина.
|
| 3963 |
-
Текущее время (Алматы): {now.strftime('%Y-%m-%d %H:%M:%S')}.
|
| 3964 |
-
Выручка за этот месяц: {monthly_revenue} {currency}.
|
| 3965 |
-
Самые просматриваемые товары (Топ-5): {views_str}.
|
| 3966 |
-
Самые продаваемые товары (Топ-5): {sales_str}.
|
| 3967 |
-
Если упоминаешь товар, используй точный формат:[POST: <product_id> Название: <product_name>].
|
| 3968 |
-
Помогай владельцу анализировать продажи и отвечать на бизнес-вопросы."""
|
| 3969 |
-
|
| 3970 |
-
try:
|
| 3971 |
-
model = genai.GenerativeModel('gemma-3-27b-it')
|
| 3972 |
-
messages =[{'role': 'user', 'parts':[{'text': sys_prompt}]}]
|
| 3973 |
-
for h in history:
|
| 3974 |
-
messages.append({'role': 'model' if h['role'] == 'ai' else 'user', 'parts':[{'text': h['text']}]})
|
| 3975 |
-
chat = model.start_chat(history=messages)
|
| 3976 |
-
resp = chat.send_message(message)
|
| 3977 |
-
return jsonify({'text': resp.text})
|
| 3978 |
-
except Exception as e:
|
| 3979 |
-
return jsonify({'text': f"Ошибка AI: {str(e)}"})
|
| 3980 |
-
|
| 3981 |
-
@app.route('/<env_id>/admin', methods=['GET', 'POST'])
|
| 3982 |
-
def admin(env_id):
|
| 3983 |
-
data = get_env_data(env_id)
|
| 3984 |
-
settings = data.get('settings', {})
|
| 3985 |
-
|
| 3986 |
-
if settings.get('admin_password_enabled') and not session.get(f'admin_auth_{env_id}'):
|
| 3987 |
-
return redirect(url_for('admin_login', env_id=env_id))
|
| 3988 |
-
|
| 3989 |
-
products = data.get('products',[])
|
| 3990 |
-
categories = data.get('categories',[])
|
| 3991 |
-
organization_info = data.get('organization_info', {})
|
| 3992 |
-
employees = data.get('employees',[])
|
| 3993 |
-
blocks = data.get('blocks',[])
|
| 3994 |
-
|
| 3995 |
-
page = request.args.get('p', 1, type=int)
|
| 3996 |
-
search_q = request.args.get('q', '').strip()
|
| 3997 |
-
|
| 3998 |
-
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 3999 |
-
data['orders'] = {}
|
| 4000 |
-
|
| 4001 |
-
if request.method == 'POST':
|
| 4002 |
-
action = request.form.get('action')
|
| 4003 |
-
try:
|
| 4004 |
-
if action == 'add_block':
|
| 4005 |
-
b_type = request.form.get('block_type')
|
| 4006 |
-
b_title = request.form.get('block_title', '').strip()
|
| 4007 |
-
b_url = request.form.get('block_url', '').strip()
|
| 4008 |
-
if b_url and not b_url.startswith(('http://', 'https://')):
|
| 4009 |
-
b_url = 'https://' + b_url
|
| 4010 |
-
b_content = request.form.get('block_content', '').strip()
|
| 4011 |
-
blocks.append({
|
| 4012 |
-
'id': uuid4().hex[:8],
|
| 4013 |
-
'type': b_type,
|
| 4014 |
-
'title': b_title,
|
| 4015 |
-
'url': b_url,
|
| 4016 |
-
'content': b_content
|
| 4017 |
-
})
|
| 4018 |
-
data['blocks'] = blocks
|
| 4019 |
-
save_env_data(env_id, data)
|
| 4020 |
-
flash("Блок добавлен.", "success")
|
| 4021 |
-
elif action == 'delete_block':
|
| 4022 |
-
b_id = request.form.get('block_id')
|
| 4023 |
-
data['blocks'] =[b for b in blocks if b.get('id') != b_id]
|
| 4024 |
-
save_env_data(env_id, data)
|
| 4025 |
-
flash("Блок удален.", "success")
|
| 4026 |
-
elif action == 'move_block_up':
|
| 4027 |
-
b_id = request.form.get('block_id')
|
| 4028 |
-
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
|
| 4029 |
-
if idx > 0:
|
| 4030 |
-
blocks[idx], blocks[idx-1] = blocks[idx-1], blocks[idx]
|
| 4031 |
-
data['blocks'] = blocks
|
| 4032 |
-
save_env_data(env_id, data)
|
| 4033 |
-
flash("Блок перемещен выше.", "success")
|
| 4034 |
-
elif action == 'move_block_down':
|
| 4035 |
-
b_id = request.form.get('block_id')
|
| 4036 |
-
idx = next((i for i, b in enumerate(blocks) if b.get('id') == b_id), -1)
|
| 4037 |
-
if idx != -1 and idx < len(blocks) - 1:
|
| 4038 |
-
blocks[idx], blocks[idx+1] = blocks[idx+1], blocks[idx]
|
| 4039 |
-
data['blocks'] = blocks
|
| 4040 |
-
save_env_data(env_id, data)
|
| 4041 |
-
flash("Блок перемещен ниже.", "success")
|
| 4042 |
-
elif action == 'add_employee':
|
| 4043 |
-
emp_name = request.form.get('emp_name', '').strip()
|
| 4044 |
-
emp_whatsapp = request.form.get('emp_whatsapp', '').strip()
|
| 4045 |
-
if emp_name and emp_whatsapp:
|
| 4046 |
-
emp_id = uuid4().hex[:8]
|
| 4047 |
-
employees.append({'id': emp_id, 'name': emp_name, 'whatsapp': emp_whatsapp})
|
| 4048 |
-
data['employees'] = employees
|
| 4049 |
-
save_env_data(env_id, data)
|
| 4050 |
-
flash("Сотрудник добавлен.", "success")
|
| 4051 |
-
elif action == 'delete_employee':
|
| 4052 |
-
emp_id = request.form.get('emp_id')
|
| 4053 |
-
employees =[e for e in employees if e.get('id') != emp_id]
|
| 4054 |
-
data['employees'] = employees
|
| 4055 |
-
save_env_data(env_id, data)
|
| 4056 |
-
flash("Сотрудник удален.", "success")
|
| 4057 |
-
elif action == 'add_category':
|
| 4058 |
-
category_name = request.form.get('category_name', '').strip()
|
| 4059 |
-
if category_name and category_name not in categories:
|
| 4060 |
-
categories.append(category_name)
|
| 4061 |
-
data['categories'] = categories
|
| 4062 |
-
save_env_data(env_id, data)
|
| 4063 |
-
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
| 4064 |
-
elif not category_name:
|
| 4065 |
-
flash("Название категории не может быть пустым.", 'error')
|
| 4066 |
-
else:
|
| 4067 |
-
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 4068 |
-
|
| 4069 |
-
elif action == 'delete_category':
|
| 4070 |
-
category_to_delete = request.form.get('category_name')
|
| 4071 |
-
if category_to_delete and category_to_delete in categories:
|
| 4072 |
-
categories.remove(category_to_delete)
|
| 4073 |
-
updated_count = 0
|
| 4074 |
-
for product in products:
|
| 4075 |
-
if product.get('category') == category_to_delete:
|
| 4076 |
-
product['category'] = 'Без категории'
|
| 4077 |
-
updated_count += 1
|
| 4078 |
-
data['categories'] = categories
|
| 4079 |
-
data['products'] = products
|
| 4080 |
-
save_env_data(env_id, data)
|
| 4081 |
-
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 4082 |
-
else:
|
| 4083 |
-
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 4084 |
-
|
| 4085 |
-
elif action == 'update_org_info':
|
| 4086 |
-
organization_info['about_us'] = request.form.get('about_us', '').strip()
|
| 4087 |
-
organization_info['shipping'] = request.form.get('shipping', '').strip()
|
| 4088 |
-
organization_info['returns'] = request.form.get('returns', '').strip()
|
| 4089 |
-
organization_info['contact'] = request.form.get('contact', '').strip()
|
| 4090 |
-
data['organization_info'] = organization_info
|
| 4091 |
-
save_env_data(env_id, data)
|
| 4092 |
-
flash("Информация о магазине успешно обновлена.", 'success')
|
| 4093 |
-
|
| 4094 |
-
elif action == 'update_settings':
|
| 4095 |
-
settings['admin_password_enabled'] = 'admin_password_enabled' in request.form
|
| 4096 |
-
settings['admin_password'] = request.form.get('admin_password', '').strip()
|
| 4097 |
-
|
| 4098 |
-
settings['organization_name'] = request.form.get('organization_name', 'Gippo312').strip()
|
| 4099 |
-
settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip()
|
| 4100 |
-
settings['currency_code'] = request.form.get('currency_code', 'KGS')
|
| 4101 |
-
settings['business_type'] = request.form.get('business_type', 'retail')
|
| 4102 |
-
settings['color_scheme'] = request.form.get('color_scheme', 'default')
|
| 4103 |
-
|
| 4104 |
-
settings['checkout_fields_enabled'] = 'checkout_fields_enabled' in request.form
|
| 4105 |
-
settings['checkout_fields'] = {
|
| 4106 |
-
'name': 'cf_name' in request.form,
|
| 4107 |
-
'phone': 'cf_phone' in request.form,
|
| 4108 |
-
'city': 'cf_city' in request.form,
|
| 4109 |
-
'address': 'cf_address' in request.form,
|
| 4110 |
-
'zip': 'cf_zip' in request.form
|
| 4111 |
-
}
|
| 4112 |
-
settings['categories_as_lines'] = 'categories_as_lines' in request.form
|
| 4113 |
-
|
| 4114 |
-
avatar_file = request.files.get('chat_avatar')
|
| 4115 |
-
if avatar_file and avatar_file.filename:
|
| 4116 |
-
if HF_TOKEN_WRITE:
|
| 4117 |
-
try:
|
| 4118 |
-
api = HfApi()
|
| 4119 |
-
old_avatar = settings.get('chat_avatar')
|
| 4120 |
-
if old_avatar:
|
| 4121 |
-
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 4122 |
-
except Exception: pass
|
| 4123 |
-
ext = os.path.splitext(avatar_file.filename)[1].lower()
|
| 4124 |
-
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
|
| 4125 |
-
uploads_dir = 'uploads_temp'
|
| 4126 |
-
os.makedirs(uploads_dir, exist_ok=True)
|
| 4127 |
-
temp_path = os.path.join(uploads_dir, avatar_filename)
|
| 4128 |
-
avatar_file.save(temp_path)
|
| 4129 |
-
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 4130 |
-
settings['chat_avatar'] = avatar_filename
|
| 4131 |
-
os.remove(temp_path)
|
| 4132 |
-
flash("Аватар успешно обновлен.", 'success')
|
| 4133 |
-
except Exception as e:
|
| 4134 |
-
flash(f"Ошибка при загрузке аватара: {e}", 'error')
|
| 4135 |
-
else:
|
| 4136 |
-
flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning")
|
| 4137 |
-
|
| 4138 |
-
data['settings'] = settings
|
| 4139 |
-
save_env_data(env_id, data)
|
| 4140 |
-
flash("Настройки успешно обновлены.", 'success')
|
| 4141 |
-
|
| 4142 |
-
elif action == 'add_product' or action == 'edit_product':
|
| 4143 |
-
product_id = request.form.get('product_id')
|
| 4144 |
-
product_data = {}
|
| 4145 |
-
is_edit = action == 'edit_product'
|
| 4146 |
-
|
| 4147 |
-
if is_edit:
|
| 4148 |
-
product_data = next((p for p in products if p.get('product_id') == product_id), None)
|
| 4149 |
-
if not product_data:
|
| 4150 |
-
flash(f"Ошибка: товар с ID {product_id} не найден.", 'error')
|
| 4151 |
-
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
|
| 4152 |
-
else:
|
| 4153 |
-
product_data['views'] = 0
|
| 4154 |
-
|
| 4155 |
-
product_data['name'] = request.form.get('name', '').strip()
|
| 4156 |
-
product_data['description'] = request.form.get('description', '').strip()
|
| 4157 |
-
category = request.form.get('category')
|
| 4158 |
-
product_data['category'] = category if category in categories else 'Без категории'
|
| 4159 |
-
|
| 4160 |
-
tags_raw = request.form.get('tags_json', '[]')
|
| 4161 |
-
try:
|
| 4162 |
-
parsed_tags = json.loads(tags_raw)
|
| 4163 |
-
for t in parsed_tags:
|
| 4164 |
-
if 'stock_batches' not in t:
|
| 4165 |
-
t['stock_batches'] =[{"qty": t.get('stock', 0), "price": t.get('price', 0), "box_price": t.get('box_price', 0)}]
|
| 4166 |
-
product_data['tags'] = parsed_tags
|
| 4167 |
-
except:
|
| 4168 |
-
product_data['tags'] =[]
|
| 4169 |
-
|
| 4170 |
-
product_data['in_stock'] = 'in_stock' in request.form
|
| 4171 |
-
product_data['is_top'] = 'is_top' in request.form
|
| 4172 |
-
|
| 4173 |
-
if not product_data['name']:
|
| 4174 |
-
flash("Название товара обязательно.", 'error')
|
| 4175 |
-
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
|
| 4176 |
-
|
| 4177 |
-
photos_files = request.files.getlist('photos')
|
| 4178 |
-
if photos_files and any(f.filename for f in photos_files):
|
| 4179 |
-
if HF_TOKEN_WRITE:
|
| 4180 |
-
uploads_dir = 'uploads_temp'
|
| 4181 |
-
os.makedirs(uploads_dir, exist_ok=True)
|
| 4182 |
-
api = HfApi()
|
| 4183 |
-
new_photos_list =[]
|
| 4184 |
-
photo_limit = 10
|
| 4185 |
-
uploaded_count = 0
|
| 4186 |
-
for photo in photos_files:
|
| 4187 |
-
if uploaded_count >= photo_limit: break
|
| 4188 |
-
if photo and photo.filename:
|
| 4189 |
-
try:
|
| 4190 |
-
ext = os.path.splitext(photo.filename)[1].lower()
|
| 4191 |
-
if ext not in['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue
|
| 4192 |
-
safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50]
|
| 4193 |
-
photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}"
|
| 4194 |
-
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 4195 |
-
photo.save(temp_path)
|
| 4196 |
-
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 4197 |
-
new_photos_list.append(photo_filename)
|
| 4198 |
-
os.remove(temp_path)
|
| 4199 |
-
uploaded_count += 1
|
| 4200 |
-
except Exception as e:
|
| 4201 |
-
flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error')
|
| 4202 |
-
if new_photos_list and is_edit and product_data.get('photos'):
|
| 4203 |
-
try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 4204 |
-
except Exception: pass
|
| 4205 |
-
if new_photos_list:
|
| 4206 |
-
product_data['photos'] = new_photos_list
|
| 4207 |
-
else:
|
| 4208 |
-
flash("HF_TOKEN не настроен. Фотографии не загружены.", "warning")
|
| 4209 |
-
|
| 4210 |
-
if is_edit:
|
| 4211 |
-
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
|
| 4212 |
-
if product_index != -1:
|
| 4213 |
-
products[product_index] = product_data
|
| 4214 |
-
flash(f"Товар '{product_data['name']}' обновлен.", 'success')
|
| 4215 |
-
else:
|
| 4216 |
-
product_data['product_id'] = uuid4().hex
|
| 4217 |
-
products.append(product_data)
|
| 4218 |
-
flash(f"Товар '{product_data['name']}' добавлен.", 'success')
|
| 4219 |
-
|
| 4220 |
-
data['products'] = products
|
| 4221 |
-
save_env_data(env_id, data)
|
| 4222 |
-
|
| 4223 |
-
elif action == 'delete_product':
|
| 4224 |
-
product_id = request.form.get('product_id')
|
| 4225 |
-
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
|
| 4226 |
-
if product_index == -1:
|
| 4227 |
-
flash(f"Ошибка удаления: товар не найден.", 'error')
|
| 4228 |
-
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
|
| 4229 |
-
deleted_product = products.pop(product_index)
|
| 4230 |
-
product_name = deleted_product.get('name', 'N/A')
|
| 4231 |
-
photos_to_delete = deleted_product.get('photos',[])
|
| 4232 |
-
if photos_to_delete and HF_TOKEN_WRITE:
|
| 4233 |
-
try:
|
| 4234 |
-
api = HfApi()
|
| 4235 |
-
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 4236 |
-
except Exception: pass
|
| 4237 |
-
data['products'] = products
|
| 4238 |
-
save_env_data(env_id, data)
|
| 4239 |
-
flash(f"Товар '{product_name}' удален.", 'success')
|
| 4240 |
-
else:
|
| 4241 |
-
flash(f"Неизвестное действие: {action}", 'warning')
|
| 4242 |
-
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
|
| 4243 |
-
except Exception as e:
|
| 4244 |
-
flash(f"Ошибка при выполнении действия.", 'error')
|
| 4245 |
-
return redirect(url_for('admin', env_id=env_id, p=page, q=search_q))
|
| 4246 |
-
|
| 4247 |
-
filtered_products = products
|
| 4248 |
-
if search_q:
|
| 4249 |
-
q_lower = search_q.lower()
|
| 4250 |
-
filtered_products =[p for p in products if q_lower in p.get('name', '').lower() or q_lower in p.get('description', '').lower()]
|
| 4251 |
-
|
| 4252 |
-
filtered_products = sorted(filtered_products, key=lambda p: p.get('name', '').lower())
|
| 4253 |
-
|
| 4254 |
-
PER_PAGE = 20
|
| 4255 |
-
total_items = len(filtered_products)
|
| 4256 |
-
total_pages = math.ceil(total_items / PER_PAGE) if total_items > 0 else 1
|
| 4257 |
-
|
| 4258 |
-
if page < 1: page = 1
|
| 4259 |
-
if page > total_pages: page = total_pages
|
| 4260 |
-
|
| 4261 |
-
start_idx = (page - 1) * PER_PAGE
|
| 4262 |
-
end_idx = start_idx + PER_PAGE
|
| 4263 |
-
paginated_products = filtered_products[start_idx:end_idx]
|
| 4264 |
-
|
| 4265 |
-
display_categories = sorted(categories)
|
| 4266 |
-
display_organization_info = organization_info
|
| 4267 |
-
display_settings = settings
|
| 4268 |
-
chat_status = { "active": False, "expires_soon": False, "expires_date": "N/A" }
|
| 4269 |
-
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
|
| 4270 |
-
|
| 4271 |
-
low_stock_count = 0
|
| 4272 |
-
if settings.get('env_mode') == '2in1':
|
| 4273 |
-
for p in products:
|
| 4274 |
-
for t in p.get('tags',[]):
|
| 4275 |
-
if t.get('stock', 0) <= 50:
|
| 4276 |
-
low_stock_count += 1
|
| 4277 |
-
|
| 4278 |
-
return render_template_string(
|
| 4279 |
-
ADMIN_TEMPLATE, paginated_products=paginated_products, total_pages=total_pages, page=page, search_q=search_q, categories=display_categories,
|
| 4280 |
-
organization_info=display_organization_info, chats={}, settings=display_settings, employees=employees,
|
| 4281 |
-
blocks=blocks, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url,
|
| 4282 |
-
currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, env_id=env_id, chat_status=chat_status, low_stock_count=low_stock_count
|
| 4283 |
-
)
|
| 4284 |
-
|
| 4285 |
-
@app.route('/generate_description_ai', methods=['POST'])
|
| 4286 |
-
def handle_generate_description_ai():
|
| 4287 |
-
request_data = request.get_json()
|
| 4288 |
-
base64_image = request_data.get('image')
|
| 4289 |
-
language = request_data.get('language', 'Русский')
|
| 4290 |
-
if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400
|
| 4291 |
-
try:
|
| 4292 |
-
image_data = base64.b64decode(base64_image)
|
| 4293 |
-
result_text = generate_ai_description_from_image(image_data, language)
|
| 4294 |
-
return jsonify({"text": result_text})
|
| 4295 |
-
except ValueError as ve: return jsonify({"error": str(ve)}), 400
|
| 4296 |
-
except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
|
| 4297 |
-
|
| 4298 |
-
if __name__ == '__main__':
|
| 4299 |
-
configure_gemini()
|
| 4300 |
-
download_db_from_hf()
|
| 4301 |
-
load_data()
|
| 4302 |
-
if HF_TOKEN_WRITE:
|
| 4303 |
-
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 4304 |
-
backup_thread.start()
|
| 4305 |
-
port = int(os.environ.get('PORT', 7860))
|
| 4306 |
-
app.run(debug=False, host='0.0.0.0', port=port)
|
| 4307 |
'qty': qty,
|
| 4308 |
'timestamp': datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'),
|
| 4309 |
'details': ''
|
|
|
|
| 3572 |
'id': uuid4().hex,
|
| 3573 |
'product_id': p_id,
|
| 3574 |
'tag_id': t_id,
|
| 3575 |
+
'type': action,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3576 |
'qty': qty,
|
| 3577 |
'timestamp': datetime.now(ALMATY_TZ).strftime('%Y-%m-%d %H:%M:%S'),
|
| 3578 |
'details': ''
|