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