Update app.py
Browse files
app.py
CHANGED
|
@@ -89,7 +89,7 @@ def load_json_data(file_key):
|
|
| 89 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 90 |
return json.load(f)
|
| 91 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 92 |
-
return
|
| 93 |
|
| 94 |
def save_json_data(file_key, data):
|
| 95 |
filepath, lock = DATA_FILES[file_key]
|
|
@@ -232,22 +232,15 @@ def sales_screen():
|
|
| 232 |
inventory = load_json_data('inventory')
|
| 233 |
kassas = load_json_data('kassas')
|
| 234 |
|
| 235 |
-
active_inventory =
|
| 236 |
for p in inventory:
|
| 237 |
-
if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',
|
| 238 |
active_inventory.append(p)
|
| 239 |
|
| 240 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
| 241 |
|
| 242 |
-
grouped_inventory = defaultdict(list)
|
| 243 |
-
for p in active_inventory:
|
| 244 |
-
first_letter = p.get('name', '#')[0].upper()
|
| 245 |
-
grouped_inventory[first_letter].append(p)
|
| 246 |
-
|
| 247 |
-
sorted_grouped_inventory = sorted(grouped_inventory.items())
|
| 248 |
-
|
| 249 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 250 |
-
return render_template_string(html, inventory=active_inventory, kassas=kassas
|
| 251 |
|
| 252 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 253 |
@admin_required
|
|
@@ -316,7 +309,7 @@ def inventory_management():
|
|
| 316 |
|
| 317 |
for product in inventory_list:
|
| 318 |
if isinstance(product, dict) and 'variants' in product:
|
| 319 |
-
for variant in product.get('variants',
|
| 320 |
stock = variant.get('stock', 0)
|
| 321 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 322 |
price = to_decimal(variant.get('price', '0'))
|
|
@@ -359,7 +352,7 @@ def edit_product(product_id):
|
|
| 359 |
inventory[i]['name'] = name
|
| 360 |
inventory[i]['barcode'] = barcode
|
| 361 |
|
| 362 |
-
new_variants =
|
| 363 |
variant_ids = request.form.getlist('variant_id[]')
|
| 364 |
variant_names = request.form.getlist('variant_name[]')
|
| 365 |
variant_prices = request.form.getlist('variant_price[]')
|
|
@@ -402,7 +395,7 @@ def edit_product(product_id):
|
|
| 402 |
def delete_product(product_id):
|
| 403 |
inventory = load_json_data('inventory')
|
| 404 |
initial_len = len(inventory)
|
| 405 |
-
inventory =
|
| 406 |
if len(inventory) < initial_len:
|
| 407 |
save_json_data('inventory', inventory)
|
| 408 |
upload_db_to_hf('inventory')
|
|
@@ -433,7 +426,7 @@ def stock_in():
|
|
| 433 |
|
| 434 |
variant_found = False
|
| 435 |
variant_name_for_log = ""
|
| 436 |
-
for i, variant in enumerate(product.get('variants',
|
| 437 |
if variant.get('id') == variant_id:
|
| 438 |
variant_name_for_log = variant.get('option_value', '')
|
| 439 |
|
|
@@ -534,7 +527,7 @@ def complete_sale():
|
|
| 534 |
if not user or not kassa:
|
| 535 |
return jsonify({'success': False, 'message': 'Кассир или касса не найдены.'}), 404
|
| 536 |
|
| 537 |
-
sale_items =
|
| 538 |
total_amount = Decimal('0.00')
|
| 539 |
inventory_updates = {}
|
| 540 |
|
|
@@ -563,7 +556,7 @@ def complete_sale():
|
|
| 563 |
if not product:
|
| 564 |
return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
|
| 565 |
|
| 566 |
-
variant = find_item_by_field(product.get('variants',
|
| 567 |
if not variant:
|
| 568 |
return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
|
| 569 |
|
|
@@ -635,7 +628,7 @@ def complete_sale():
|
|
| 635 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 636 |
kassas[i]['balance'] = str(current_balance + total_amount)
|
| 637 |
if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
|
| 638 |
-
kassas[i]['history'] =
|
| 639 |
kassas[i]['history'].append({
|
| 640 |
'type': 'sale',
|
| 641 |
'amount': str(total_amount),
|
|
@@ -686,13 +679,13 @@ def transaction_history():
|
|
| 686 |
transactions = load_json_data('transactions')
|
| 687 |
kassas = load_json_data('kassas')
|
| 688 |
|
| 689 |
-
filtered_transactions =
|
| 690 |
t for t in transactions
|
| 691 |
if datetime.fromisoformat(t['timestamp']).date() == selected_date
|
| 692 |
]
|
| 693 |
|
| 694 |
if selected_kassa_id:
|
| 695 |
-
filtered_transactions =
|
| 696 |
t for t in filtered_transactions
|
| 697 |
if t.get('kassa_id') == selected_kassa_id
|
| 698 |
]
|
|
@@ -702,7 +695,7 @@ def transaction_history():
|
|
| 702 |
total_quantity_sold = 0
|
| 703 |
for t in filtered_transactions:
|
| 704 |
if t.get('type') == 'sale':
|
| 705 |
-
for item in t.get('items',
|
| 706 |
total_quantity_sold += int(item.get('quantity', 0))
|
| 707 |
|
| 708 |
filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
@@ -715,7 +708,7 @@ def transaction_history():
|
|
| 715 |
def edit_transaction(transaction_id):
|
| 716 |
try:
|
| 717 |
data = request.get_json()
|
| 718 |
-
items_update = data.get('items',
|
| 719 |
|
| 720 |
transactions = load_json_data('transactions')
|
| 721 |
kassas = load_json_data('kassas')
|
|
@@ -770,7 +763,7 @@ def edit_transaction(transaction_id):
|
|
| 770 |
for i, k in enumerate(kassas):
|
| 771 |
if k.get('id') == kassa_id:
|
| 772 |
k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
|
| 773 |
-
k.setdefault('history',
|
| 774 |
'type': 'correction',
|
| 775 |
'amount': str(amount_diff),
|
| 776 |
'timestamp': get_current_time().isoformat(),
|
|
@@ -801,14 +794,14 @@ def delete_transaction(transaction_id):
|
|
| 801 |
flash("Транзакция не найдена.", "danger")
|
| 802 |
return redirect(url_for('transaction_history'))
|
| 803 |
|
| 804 |
-
for item in transaction_to_delete.get('items',
|
| 805 |
if item.get('is_custom'):
|
| 806 |
continue
|
| 807 |
|
| 808 |
product = find_item_by_field(inventory, 'id', item.get('product_id'))
|
| 809 |
if not product: continue
|
| 810 |
|
| 811 |
-
variant = find_item_by_field(product.get('variants',
|
| 812 |
if not variant: continue
|
| 813 |
|
| 814 |
quantity_change = item.get('quantity', 0)
|
|
@@ -826,14 +819,14 @@ def delete_transaction(transaction_id):
|
|
| 826 |
|
| 827 |
kassa['balance'] = str(current_balance - amount_change)
|
| 828 |
|
| 829 |
-
kassa.setdefault('history',
|
| 830 |
'type': 'deletion',
|
| 831 |
'amount': str(-amount_change),
|
| 832 |
'timestamp': get_current_time().isoformat(),
|
| 833 |
'description': f"Удаление транзакции {transaction_id[:8]}"
|
| 834 |
})
|
| 835 |
|
| 836 |
-
transactions =
|
| 837 |
|
| 838 |
if transaction_to_delete.get('type') == 'return':
|
| 839 |
original_id = transaction_to_delete.get('original_transaction_id')
|
|
@@ -870,15 +863,15 @@ def reports():
|
|
| 870 |
personal_expenses = load_json_data('personal_expenses')
|
| 871 |
users = load_json_data('users')
|
| 872 |
|
| 873 |
-
filtered_transactions =
|
| 874 |
t for t in transactions
|
| 875 |
if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
|
| 876 |
]
|
| 877 |
-
filtered_expenses =
|
| 878 |
e for e in expenses
|
| 879 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 880 |
]
|
| 881 |
-
filtered_personal_expenses =
|
| 882 |
e for e in personal_expenses
|
| 883 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 884 |
]
|
|
@@ -949,10 +942,10 @@ def product_roi_report():
|
|
| 949 |
inventory = load_json_data('inventory')
|
| 950 |
transactions = load_json_data('transactions')
|
| 951 |
|
| 952 |
-
product_stats =
|
| 953 |
|
| 954 |
for product in inventory:
|
| 955 |
-
for variant in product.get('variants',
|
| 956 |
total_revenue = Decimal('0.00')
|
| 957 |
total_cogs = Decimal('0.00')
|
| 958 |
total_qty_sold = 0
|
|
@@ -1087,7 +1080,7 @@ def manage_kassa():
|
|
| 1087 |
elif action == 'delete':
|
| 1088 |
kassa_id = request.form.get('id')
|
| 1089 |
initial_len = len(kassas)
|
| 1090 |
-
kassas =
|
| 1091 |
if len(kassas) < initial_len:
|
| 1092 |
flash("Касса удалена.", "success")
|
| 1093 |
else:
|
|
@@ -1125,7 +1118,7 @@ def kassa_operation():
|
|
| 1125 |
new_balance -= amount
|
| 1126 |
|
| 1127 |
kassas[i]['balance'] = str(new_balance)
|
| 1128 |
-
if 'history' not in kassas[i]: kassas[i]['history'] =
|
| 1129 |
kassas[i]['history'].append({
|
| 1130 |
'type': op_type,
|
| 1131 |
'amount': str(amount),
|
|
@@ -1209,7 +1202,7 @@ def manage_personal_expense():
|
|
| 1209 |
def delete_personal_expense(expense_id):
|
| 1210 |
expenses = load_json_data('personal_expenses')
|
| 1211 |
initial_len = len(expenses)
|
| 1212 |
-
expenses =
|
| 1213 |
if len(expenses) < initial_len:
|
| 1214 |
save_json_data('personal_expenses', expenses)
|
| 1215 |
upload_db_to_hf('personal_expenses')
|
|
@@ -1301,7 +1294,7 @@ def end_shift():
|
|
| 1301 |
kassa = find_item_by_field(kassas, 'id', shift['kassa_id'])
|
| 1302 |
shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
|
| 1303 |
|
| 1304 |
-
shift_transactions =
|
| 1305 |
t for t in transactions
|
| 1306 |
if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
|
| 1307 |
]
|
|
@@ -1332,7 +1325,7 @@ def cashier_dashboard(user_id):
|
|
| 1332 |
abort(404, "Кассир не найден")
|
| 1333 |
|
| 1334 |
transactions = load_json_data('transactions')
|
| 1335 |
-
user_transactions =
|
| 1336 |
user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 1337 |
|
| 1338 |
html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
|
|
@@ -1360,14 +1353,14 @@ def return_transaction(transaction_id):
|
|
| 1360 |
|
| 1361 |
total_amount = to_decimal(original_transaction['total_amount'])
|
| 1362 |
|
| 1363 |
-
return_items =
|
| 1364 |
inventory_updates = {}
|
| 1365 |
for item in original_transaction['items']:
|
| 1366 |
return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
|
| 1367 |
if not item.get('is_custom'):
|
| 1368 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 1369 |
if product:
|
| 1370 |
-
variant = find_item_by_field(product.get('variants',
|
| 1371 |
if variant:
|
| 1372 |
inventory_updates[item['variant_id']] = {'product_id': item['product_id'], 'new_stock': variant.get('stock', 0) + item['quantity']}
|
| 1373 |
|
|
@@ -1398,7 +1391,7 @@ def return_transaction(transaction_id):
|
|
| 1398 |
for variant_id, update_info in inventory_updates.items():
|
| 1399 |
for p in inventory:
|
| 1400 |
if p.get('id') == update_info['product_id']:
|
| 1401 |
-
for v in p.get('variants',
|
| 1402 |
if v.get('id') == variant_id:
|
| 1403 |
v['stock'] = update_info['new_stock']
|
| 1404 |
p['timestamp_updated'] = now_iso
|
|
@@ -1410,7 +1403,7 @@ def return_transaction(transaction_id):
|
|
| 1410 |
if k['id'] == original_transaction['kassa_id']:
|
| 1411 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1412 |
kassas[i]['balance'] = str(current_balance - total_amount)
|
| 1413 |
-
kassas[i].setdefault('history',
|
| 1414 |
'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
|
| 1415 |
'transaction_id': return_transaction['id']
|
| 1416 |
})
|
|
@@ -1440,7 +1433,7 @@ def backup_hf():
|
|
| 1440 |
@app.route('/download', methods=['GET'])
|
| 1441 |
@admin_required
|
| 1442 |
def download_hf():
|
| 1443 |
-
errors =
|
| 1444 |
success_count = 0
|
| 1445 |
for key in DATA_FILES.keys():
|
| 1446 |
filepath, _ = DATA_FILES[key]
|
|
@@ -1500,13 +1493,10 @@ BASE_TEMPLATE = """
|
|
| 1500 |
.sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
|
| 1501 |
.sidebar.active { transform: translateX(0); }
|
| 1502 |
.main-content { margin-left: 0; }
|
| 1503 |
-
}
|
| 1504 |
-
[data-bs-theme="dark"]
|
| 1505 |
-
[data-bs-theme="dark"] .
|
| 1506 |
-
[data-bs-theme="dark"] .accordion-button {
|
| 1507 |
-
[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
|
| 1508 |
-
[data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }
|
| 1509 |
-
[data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
|
| 1510 |
[data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
|
| 1511 |
.product-card { cursor: pointer; }
|
| 1512 |
.product-card:hover { border-color: var(--bs-primary); }
|
|
@@ -1525,7 +1515,7 @@ BASE_TEMPLATE = """
|
|
| 1525 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'inventory_management' %}active{% endif %}" href="{{ url_for('inventory_management') }}"><i class="fas fa-fw fa-boxes me-2"></i>Склад</a></li>
|
| 1526 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'transaction_history' %}active{% endif %}" href="{{ url_for('transaction_history') }}"><i class="fas fa-fw fa-history me-2"></i>Транзакции</a></li>
|
| 1527 |
<li class="nav-item dropdown">
|
| 1528 |
-
<a class="nav-link dropdown-toggle {% if request.endpoint in
|
| 1529 |
<i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
|
| 1530 |
</a>
|
| 1531 |
<ul class="dropdown-menu dropdown-menu-dark">
|
|
@@ -1533,8 +1523,8 @@ BASE_TEMPLATE = """
|
|
| 1533 |
<li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
|
| 1534 |
</ul>
|
| 1535 |
</li>
|
| 1536 |
-
<li class="nav-item"><a class="nav-link {% if request.endpoint in
|
| 1537 |
-
<li class="nav-item"><a class="nav-link {% if request.endpoint in
|
| 1538 |
{% if session.admin_logged_in %}
|
| 1539 |
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_logout') }}"><i class="fas fa-fw fa-sign-out-alt me-2"></i>Выйти (Админ)</a></li>
|
| 1540 |
{% endif %}
|
|
@@ -1651,36 +1641,7 @@ SALES_SCREEN_CONTENT = """
|
|
| 1651 |
</div>
|
| 1652 |
<input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
|
| 1653 |
|
| 1654 |
-
<div id="product-
|
| 1655 |
-
{% for letter, products in grouped_inventory %}
|
| 1656 |
-
<div class="accordion-item">
|
| 1657 |
-
<h2 class="accordion-header" id="heading-{{ letter }}">
|
| 1658 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ letter }}" aria-expanded="false" aria-controls="collapse-{{ letter }}">
|
| 1659 |
-
{{ letter }}
|
| 1660 |
-
</button>
|
| 1661 |
-
</h2>
|
| 1662 |
-
<div id="collapse-{{ letter }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ letter }}" data-bs-parent="#product-accordion">
|
| 1663 |
-
<div class="accordion-body d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
|
| 1664 |
-
{% for p in products %}
|
| 1665 |
-
<div class="card text-center product-card" data-barcode="{{ p.barcode }}">
|
| 1666 |
-
<div class="card-body p-2">
|
| 1667 |
-
<h6 class="card-title small mb-1">{{ p.name }}</h6>
|
| 1668 |
-
<p class="card-text fw-bold mb-0">
|
| 1669 |
-
{% if p.variants|length > 1 %}
|
| 1670 |
-
от {{ format_currency_py(p.variants|map(attribute='price')|min) }} с
|
| 1671 |
-
{% elif p.variants|length == 1 %}
|
| 1672 |
-
{{ format_currency_py(p.variants[0].price) }} с
|
| 1673 |
-
{% else %}
|
| 1674 |
-
Нет в наличии
|
| 1675 |
-
{% endif %}
|
| 1676 |
-
</p>
|
| 1677 |
-
</div>
|
| 1678 |
-
</div>
|
| 1679 |
-
{% endfor %}
|
| 1680 |
-
</div>
|
| 1681 |
-
</div>
|
| 1682 |
-
</div>
|
| 1683 |
-
{% endfor %}
|
| 1684 |
</div>
|
| 1685 |
</div>
|
| 1686 |
</div>
|
|
@@ -1764,12 +1725,14 @@ SALES_SCREEN_SCRIPTS = """
|
|
| 1764 |
<script>
|
| 1765 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1766 |
const cart = {};
|
| 1767 |
-
const productGrid = document.getElementById('product-
|
| 1768 |
const cartItemsEl = document.getElementById('cart-items');
|
| 1769 |
const cartTotalEl = document.getElementById('cart-total');
|
| 1770 |
let audioCtx;
|
| 1771 |
let isScannerPaused = false;
|
| 1772 |
|
|
|
|
|
|
|
| 1773 |
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
| 1774 |
const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
|
| 1775 |
const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
|
|
@@ -1803,6 +1766,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1803 |
return parseFloat(String(stringNumber).replace(/\\s/g, '').replace(',', '.')) || 0;
|
| 1804 |
}
|
| 1805 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1806 |
const updateCartView = () => {
|
| 1807 |
cartItemsEl.innerHTML = '';
|
| 1808 |
let total = 0;
|
|
@@ -1941,32 +1938,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1941 |
updateCartView();
|
| 1942 |
});
|
| 1943 |
|
| 1944 |
-
document.getElementById('product-search').addEventListener('input', e => {
|
| 1945 |
-
const term = e.target.value.toLowerCase();
|
| 1946 |
-
const productCards = document.querySelectorAll('#product-accordion .product-card');
|
| 1947 |
-
productCards.forEach(card => {
|
| 1948 |
-
const productName = card.querySelector('.card-title').textContent.toLowerCase();
|
| 1949 |
-
const barcode = card.dataset.barcode.toLowerCase();
|
| 1950 |
-
const show = productName.includes(term) || barcode.includes(term);
|
| 1951 |
-
card.style.display = show ? '' : 'none';
|
| 1952 |
-
});
|
| 1953 |
-
document.querySelectorAll('#product-accordion .accordion-item').forEach(accordionItem => {
|
| 1954 |
-
const collapseElement = accordionItem.querySelector('.accordion-collapse');
|
| 1955 |
-
const matchingCardsInGroup = accordionItem.querySelectorAll('.product-card:not([style*="display: none"])');
|
| 1956 |
-
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(collapseElement, { toggle: false });
|
| 1957 |
-
|
| 1958 |
-
if (term === '') {
|
| 1959 |
-
bsCollapse.hide();
|
| 1960 |
-
} else {
|
| 1961 |
-
if (matchingCardsInGroup.length > 0) {
|
| 1962 |
-
bsCollapse.show();
|
| 1963 |
-
} else {
|
| 1964 |
-
bsCollapse.hide();
|
| 1965 |
-
}
|
| 1966 |
-
}
|
| 1967 |
-
});
|
| 1968 |
-
});
|
| 1969 |
-
|
| 1970 |
const completeSale = (paymentMethod) => {
|
| 1971 |
if (!session.shift || !session.cashier || !session.kassa) {
|
| 1972 |
alert('Смена не активна. Начните смену, чтобы проводить продажи.');
|
|
@@ -2354,6 +2325,14 @@ INVENTORY_CONTENT = """
|
|
| 2354 |
<div class="modal-header"><h5 class="modal-title">Оприходование товара</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
| 2355 |
<form action="{{ url_for('stock_in') }}" method="POST">
|
| 2356 |
<div class="modal-body">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2357 |
<div class="mb-3">
|
| 2358 |
<label for="stockin-product" class="form-label">Товар</label>
|
| 2359 |
<select id="stockin-product" name="product_id" class="form-select" required>
|
|
@@ -2402,31 +2381,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2402 |
document.querySelectorAll('.scan-modal-btn').forEach(btn => {
|
| 2403 |
btn.addEventListener('click', e => {
|
| 2404 |
const form = e.target.closest('form');
|
| 2405 |
-
|
| 2406 |
-
|
| 2407 |
-
|
| 2408 |
-
|
| 2409 |
-
|
| 2410 |
-
|
| 2411 |
-
|
| 2412 |
-
|
|
|
|
| 2413 |
}
|
| 2414 |
-
scannerContainer.style.display = 'block';
|
| 2415 |
-
currentScannerContainer = scannerContainer;
|
| 2416 |
-
const scannerId = scannerContainer.id + '-reader';
|
| 2417 |
-
if(!document.getElementById(scannerId)) scannerContainer.innerHTML = `<div id="${scannerId}" style="width: 100%;"></div>`;
|
| 2418 |
-
|
| 2419 |
-
const html5QrCode = new Html5Qrcode(scannerId);
|
| 2420 |
-
currentScanner = html5QrCode;
|
| 2421 |
-
const onScanSuccess = (decodedText, decodedResult) => {
|
| 2422 |
-
barcodeInput.value = decodedText;
|
| 2423 |
-
try { html5QrCode.stop(); } catch(e) {}
|
| 2424 |
-
currentScanner = null;
|
| 2425 |
-
scannerContainer.style.display = 'none';
|
| 2426 |
-
};
|
| 2427 |
-
html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } }, onScanSuccess);
|
| 2428 |
});
|
| 2429 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2430 |
|
| 2431 |
document.querySelectorAll('.modal').forEach(modal => {
|
| 2432 |
modal.addEventListener('hidden.bs.modal', () => {
|
|
@@ -2452,7 +2442,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2452 |
const formData = new FormData();
|
| 2453 |
formData.append('image', file);
|
| 2454 |
|
| 2455 |
-
preview.src = "data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
|
| 2456 |
|
| 2457 |
fetch("{{ url_for('upload_image') }}", {
|
| 2458 |
method: 'POST',
|
|
@@ -2528,6 +2518,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2528 |
const inventoryData = JSON.parse('{{ inventory|tojson|safe }}');
|
| 2529 |
const productSelect = document.getElementById('stockin-product');
|
| 2530 |
const variantSelect = document.getElementById('stockin-variant');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2531 |
|
| 2532 |
productSelect.addEventListener('change', () => {
|
| 2533 |
const productId = productSelect.value;
|
|
@@ -2692,7 +2694,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2692 |
|
| 2693 |
document.getElementById('save-trans-btn').addEventListener('click', () => {
|
| 2694 |
const form = document.getElementById('edit-trans-form');
|
| 2695 |
-
const items_update =
|
| 2696 |
form.querySelectorAll('tbody tr').forEach(row => {
|
| 2697 |
items_update.push({
|
| 2698 |
id: row.dataset.itemId,
|
|
|
|
| 89 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 90 |
return json.load(f)
|
| 91 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 92 |
+
return[]
|
| 93 |
|
| 94 |
def save_json_data(file_key, data):
|
| 95 |
filepath, lock = DATA_FILES[file_key]
|
|
|
|
| 232 |
inventory = load_json_data('inventory')
|
| 233 |
kassas = load_json_data('kassas')
|
| 234 |
|
| 235 |
+
active_inventory =[]
|
| 236 |
for p in inventory:
|
| 237 |
+
if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',[])):
|
| 238 |
active_inventory.append(p)
|
| 239 |
|
| 240 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 243 |
+
return render_template_string(html, inventory=active_inventory, kassas=kassas)
|
| 244 |
|
| 245 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 246 |
@admin_required
|
|
|
|
| 309 |
|
| 310 |
for product in inventory_list:
|
| 311 |
if isinstance(product, dict) and 'variants' in product:
|
| 312 |
+
for variant in product.get('variants',[]):
|
| 313 |
stock = variant.get('stock', 0)
|
| 314 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 315 |
price = to_decimal(variant.get('price', '0'))
|
|
|
|
| 352 |
inventory[i]['name'] = name
|
| 353 |
inventory[i]['barcode'] = barcode
|
| 354 |
|
| 355 |
+
new_variants =[]
|
| 356 |
variant_ids = request.form.getlist('variant_id[]')
|
| 357 |
variant_names = request.form.getlist('variant_name[]')
|
| 358 |
variant_prices = request.form.getlist('variant_price[]')
|
|
|
|
| 395 |
def delete_product(product_id):
|
| 396 |
inventory = load_json_data('inventory')
|
| 397 |
initial_len = len(inventory)
|
| 398 |
+
inventory =[p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
|
| 399 |
if len(inventory) < initial_len:
|
| 400 |
save_json_data('inventory', inventory)
|
| 401 |
upload_db_to_hf('inventory')
|
|
|
|
| 426 |
|
| 427 |
variant_found = False
|
| 428 |
variant_name_for_log = ""
|
| 429 |
+
for i, variant in enumerate(product.get('variants',[])):
|
| 430 |
if variant.get('id') == variant_id:
|
| 431 |
variant_name_for_log = variant.get('option_value', '')
|
| 432 |
|
|
|
|
| 527 |
if not user or not kassa:
|
| 528 |
return jsonify({'success': False, 'message': 'Кассир или касса не найдены.'}), 404
|
| 529 |
|
| 530 |
+
sale_items =[]
|
| 531 |
total_amount = Decimal('0.00')
|
| 532 |
inventory_updates = {}
|
| 533 |
|
|
|
|
| 556 |
if not product:
|
| 557 |
return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
|
| 558 |
|
| 559 |
+
variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
|
| 560 |
if not variant:
|
| 561 |
return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
|
| 562 |
|
|
|
|
| 628 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 629 |
kassas[i]['balance'] = str(current_balance + total_amount)
|
| 630 |
if 'history' not in kassas[i] or not isinstance(kassas[i]['history'], list):
|
| 631 |
+
kassas[i]['history'] =[]
|
| 632 |
kassas[i]['history'].append({
|
| 633 |
'type': 'sale',
|
| 634 |
'amount': str(total_amount),
|
|
|
|
| 679 |
transactions = load_json_data('transactions')
|
| 680 |
kassas = load_json_data('kassas')
|
| 681 |
|
| 682 |
+
filtered_transactions =[
|
| 683 |
t for t in transactions
|
| 684 |
if datetime.fromisoformat(t['timestamp']).date() == selected_date
|
| 685 |
]
|
| 686 |
|
| 687 |
if selected_kassa_id:
|
| 688 |
+
filtered_transactions =[
|
| 689 |
t for t in filtered_transactions
|
| 690 |
if t.get('kassa_id') == selected_kassa_id
|
| 691 |
]
|
|
|
|
| 695 |
total_quantity_sold = 0
|
| 696 |
for t in filtered_transactions:
|
| 697 |
if t.get('type') == 'sale':
|
| 698 |
+
for item in t.get('items',[]):
|
| 699 |
total_quantity_sold += int(item.get('quantity', 0))
|
| 700 |
|
| 701 |
filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
|
|
| 708 |
def edit_transaction(transaction_id):
|
| 709 |
try:
|
| 710 |
data = request.get_json()
|
| 711 |
+
items_update = data.get('items',[])
|
| 712 |
|
| 713 |
transactions = load_json_data('transactions')
|
| 714 |
kassas = load_json_data('kassas')
|
|
|
|
| 763 |
for i, k in enumerate(kassas):
|
| 764 |
if k.get('id') == kassa_id:
|
| 765 |
k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
|
| 766 |
+
k.setdefault('history',[]).append({
|
| 767 |
'type': 'correction',
|
| 768 |
'amount': str(amount_diff),
|
| 769 |
'timestamp': get_current_time().isoformat(),
|
|
|
|
| 794 |
flash("Транзакция не найдена.", "danger")
|
| 795 |
return redirect(url_for('transaction_history'))
|
| 796 |
|
| 797 |
+
for item in transaction_to_delete.get('items',[]):
|
| 798 |
if item.get('is_custom'):
|
| 799 |
continue
|
| 800 |
|
| 801 |
product = find_item_by_field(inventory, 'id', item.get('product_id'))
|
| 802 |
if not product: continue
|
| 803 |
|
| 804 |
+
variant = find_item_by_field(product.get('variants',[]), 'id', item.get('variant_id'))
|
| 805 |
if not variant: continue
|
| 806 |
|
| 807 |
quantity_change = item.get('quantity', 0)
|
|
|
|
| 819 |
|
| 820 |
kassa['balance'] = str(current_balance - amount_change)
|
| 821 |
|
| 822 |
+
kassa.setdefault('history',[]).append({
|
| 823 |
'type': 'deletion',
|
| 824 |
'amount': str(-amount_change),
|
| 825 |
'timestamp': get_current_time().isoformat(),
|
| 826 |
'description': f"Удаление транзакции {transaction_id[:8]}"
|
| 827 |
})
|
| 828 |
|
| 829 |
+
transactions =[t for t in transactions if t.get('id') != transaction_id]
|
| 830 |
|
| 831 |
if transaction_to_delete.get('type') == 'return':
|
| 832 |
original_id = transaction_to_delete.get('original_transaction_id')
|
|
|
|
| 863 |
personal_expenses = load_json_data('personal_expenses')
|
| 864 |
users = load_json_data('users')
|
| 865 |
|
| 866 |
+
filtered_transactions =[
|
| 867 |
t for t in transactions
|
| 868 |
if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
|
| 869 |
]
|
| 870 |
+
filtered_expenses =[
|
| 871 |
e for e in expenses
|
| 872 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 873 |
]
|
| 874 |
+
filtered_personal_expenses =[
|
| 875 |
e for e in personal_expenses
|
| 876 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 877 |
]
|
|
|
|
| 942 |
inventory = load_json_data('inventory')
|
| 943 |
transactions = load_json_data('transactions')
|
| 944 |
|
| 945 |
+
product_stats =[]
|
| 946 |
|
| 947 |
for product in inventory:
|
| 948 |
+
for variant in product.get('variants',[]):
|
| 949 |
total_revenue = Decimal('0.00')
|
| 950 |
total_cogs = Decimal('0.00')
|
| 951 |
total_qty_sold = 0
|
|
|
|
| 1080 |
elif action == 'delete':
|
| 1081 |
kassa_id = request.form.get('id')
|
| 1082 |
initial_len = len(kassas)
|
| 1083 |
+
kassas =[k for k in kassas if k.get('id') != kassa_id]
|
| 1084 |
if len(kassas) < initial_len:
|
| 1085 |
flash("Касса удалена.", "success")
|
| 1086 |
else:
|
|
|
|
| 1118 |
new_balance -= amount
|
| 1119 |
|
| 1120 |
kassas[i]['balance'] = str(new_balance)
|
| 1121 |
+
if 'history' not in kassas[i]: kassas[i]['history'] =[]
|
| 1122 |
kassas[i]['history'].append({
|
| 1123 |
'type': op_type,
|
| 1124 |
'amount': str(amount),
|
|
|
|
| 1202 |
def delete_personal_expense(expense_id):
|
| 1203 |
expenses = load_json_data('personal_expenses')
|
| 1204 |
initial_len = len(expenses)
|
| 1205 |
+
expenses =[e for e in expenses if e.get('id') != expense_id]
|
| 1206 |
if len(expenses) < initial_len:
|
| 1207 |
save_json_data('personal_expenses', expenses)
|
| 1208 |
upload_db_to_hf('personal_expenses')
|
|
|
|
| 1294 |
kassa = find_item_by_field(kassas, 'id', shift['kassa_id'])
|
| 1295 |
shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
|
| 1296 |
|
| 1297 |
+
shift_transactions =[
|
| 1298 |
t for t in transactions
|
| 1299 |
if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
|
| 1300 |
]
|
|
|
|
| 1325 |
abort(404, "Кассир не найден")
|
| 1326 |
|
| 1327 |
transactions = load_json_data('transactions')
|
| 1328 |
+
user_transactions =[t for t in transactions if t.get('user_id') == user_id]
|
| 1329 |
user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 1330 |
|
| 1331 |
html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
|
|
|
|
| 1353 |
|
| 1354 |
total_amount = to_decimal(original_transaction['total_amount'])
|
| 1355 |
|
| 1356 |
+
return_items =[]
|
| 1357 |
inventory_updates = {}
|
| 1358 |
for item in original_transaction['items']:
|
| 1359 |
return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
|
| 1360 |
if not item.get('is_custom'):
|
| 1361 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 1362 |
if product:
|
| 1363 |
+
variant = find_item_by_field(product.get('variants',[]), 'id', item['variant_id'])
|
| 1364 |
if variant:
|
| 1365 |
inventory_updates[item['variant_id']] = {'product_id': item['product_id'], 'new_stock': variant.get('stock', 0) + item['quantity']}
|
| 1366 |
|
|
|
|
| 1391 |
for variant_id, update_info in inventory_updates.items():
|
| 1392 |
for p in inventory:
|
| 1393 |
if p.get('id') == update_info['product_id']:
|
| 1394 |
+
for v in p.get('variants',[]):
|
| 1395 |
if v.get('id') == variant_id:
|
| 1396 |
v['stock'] = update_info['new_stock']
|
| 1397 |
p['timestamp_updated'] = now_iso
|
|
|
|
| 1403 |
if k['id'] == original_transaction['kassa_id']:
|
| 1404 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1405 |
kassas[i]['balance'] = str(current_balance - total_amount)
|
| 1406 |
+
kassas[i].setdefault('history',[]).append({
|
| 1407 |
'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
|
| 1408 |
'transaction_id': return_transaction['id']
|
| 1409 |
})
|
|
|
|
| 1433 |
@app.route('/download', methods=['GET'])
|
| 1434 |
@admin_required
|
| 1435 |
def download_hf():
|
| 1436 |
+
errors =[]
|
| 1437 |
success_count = 0
|
| 1438 |
for key in DATA_FILES.keys():
|
| 1439 |
filepath, _ = DATA_FILES[key]
|
|
|
|
| 1493 |
.sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
|
| 1494 |
.sidebar.active { transform: translateX(0); }
|
| 1495 |
.main-content { margin-left: 0; }
|
| 1496 |
+
}[data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
|
| 1497 |
+
[data-bs-theme="dark"] .card, [data-bs-theme="dark"] .modal-content, [data-bs-theme="dark"] .list-group-item,[data-bs-theme="dark"] .table, [data-bs-theme="dark"] .accordion-item { background-color: #343a40; }
|
| 1498 |
+
[data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
|
| 1499 |
+
[data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }[data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
|
|
|
|
|
|
|
|
|
|
| 1500 |
[data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
|
| 1501 |
.product-card { cursor: pointer; }
|
| 1502 |
.product-card:hover { border-color: var(--bs-primary); }
|
|
|
|
| 1515 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'inventory_management' %}active{% endif %}" href="{{ url_for('inventory_management') }}"><i class="fas fa-fw fa-boxes me-2"></i>Склад</a></li>
|
| 1516 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'transaction_history' %}active{% endif %}" href="{{ url_for('transaction_history') }}"><i class="fas fa-fw fa-history me-2"></i>Транзакции</a></li>
|
| 1517 |
<li class="nav-item dropdown">
|
| 1518 |
+
<a class="nav-link dropdown-toggle {% if request.endpoint in['reports', 'product_roi_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 1519 |
<i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
|
| 1520 |
</a>
|
| 1521 |
<ul class="dropdown-menu dropdown-menu-dark">
|
|
|
|
| 1523 |
<li><a class="dropdown-item" href="{{ url_for('product_roi_report') }}">Окупаемость товаров</a></li>
|
| 1524 |
</ul>
|
| 1525 |
</li>
|
| 1526 |
+
<li class="nav-item"><a class="nav-link {% if request.endpoint in['cashier_login', 'cashier_dashboard'] %}active{% endif %}" href="{{ url_for('cashier_login') }}"><i class="fas fa-fw fa-user-circle me-2"></i>Кабинет кассира</a></li>
|
| 1527 |
+
<li class="nav-item"><a class="nav-link {% if request.endpoint in['admin_panel', 'admin_shifts'] %}active{% endif %}" href="{{ url_for('admin_panel') }}"><i class="fas fa-fw fa-cogs me-2"></i>Админка</a></li>
|
| 1528 |
{% if session.admin_logged_in %}
|
| 1529 |
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_logout') }}"><i class="fas fa-fw fa-sign-out-alt me-2"></i>Выйти (Админ)</a></li>
|
| 1530 |
{% endif %}
|
|
|
|
| 1641 |
</div>
|
| 1642 |
<input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
|
| 1643 |
|
| 1644 |
+
<div id="product-grid" class="d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); max-height: 65vh; overflow-y: auto;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1645 |
</div>
|
| 1646 |
</div>
|
| 1647 |
</div>
|
|
|
|
| 1725 |
<script>
|
| 1726 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1727 |
const cart = {};
|
| 1728 |
+
const productGrid = document.getElementById('product-grid');
|
| 1729 |
const cartItemsEl = document.getElementById('cart-items');
|
| 1730 |
const cartTotalEl = document.getElementById('cart-total');
|
| 1731 |
let audioCtx;
|
| 1732 |
let isScannerPaused = false;
|
| 1733 |
|
| 1734 |
+
const activeInventory = JSON.parse('{{ inventory|tojson|safe }}');
|
| 1735 |
+
|
| 1736 |
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
| 1737 |
const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
|
| 1738 |
const cashierLoginModal = new bootstrap.Modal(document.getElementById('cashierLoginModal'));
|
|
|
|
| 1766 |
return parseFloat(String(stringNumber).replace(/\\s/g, '').replace(',', '.')) || 0;
|
| 1767 |
}
|
| 1768 |
|
| 1769 |
+
const renderProducts = (products) => {
|
| 1770 |
+
const toRender = products.slice(0, 200);
|
| 1771 |
+
productGrid.innerHTML = toRender.map(p => {
|
| 1772 |
+
let priceStr = 'Нет в наличии';
|
| 1773 |
+
if(p.variants && p.variants.length > 1) {
|
| 1774 |
+
const minPrice = Math.min(...p.variants.map(v => parseLocaleNumber(v.price)));
|
| 1775 |
+
priceStr = 'от ' + minPrice.toLocaleString('ru-RU', {minimumFractionDigits: 2}) + ' с';
|
| 1776 |
+
} else if(p.variants && p.variants.length === 1) {
|
| 1777 |
+
priceStr = parseLocaleNumber(p.variants[0].price).toLocaleString('ru-RU', {minimumFractionDigits: 2}) + ' с';
|
| 1778 |
+
}
|
| 1779 |
+
return `<div class="card text-center product-card" data-barcode="${p.barcode}">
|
| 1780 |
+
<div class="card-body p-2">
|
| 1781 |
+
<h6 class="card-title small mb-1">${p.name}</h6>
|
| 1782 |
+
<p class="card-text fw-bold mb-0">${priceStr}</p>
|
| 1783 |
+
</div>
|
| 1784 |
+
</div>`;
|
| 1785 |
+
}).join('');
|
| 1786 |
+
};
|
| 1787 |
+
|
| 1788 |
+
renderProducts(activeInventory);
|
| 1789 |
+
|
| 1790 |
+
document.getElementById('product-search').addEventListener('input', e => {
|
| 1791 |
+
const term = e.target.value.toLowerCase().trim();
|
| 1792 |
+
if(!term) {
|
| 1793 |
+
renderProducts(activeInventory);
|
| 1794 |
+
return;
|
| 1795 |
+
}
|
| 1796 |
+
const filtered = activeInventory.filter(p =>
|
| 1797 |
+
(p.name && p.name.toLowerCase().includes(term)) ||
|
| 1798 |
+
(p.barcode && p.barcode.toLowerCase().includes(term))
|
| 1799 |
+
);
|
| 1800 |
+
renderProducts(filtered);
|
| 1801 |
+
});
|
| 1802 |
+
|
| 1803 |
const updateCartView = () => {
|
| 1804 |
cartItemsEl.innerHTML = '';
|
| 1805 |
let total = 0;
|
|
|
|
| 1938 |
updateCartView();
|
| 1939 |
});
|
| 1940 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1941 |
const completeSale = (paymentMethod) => {
|
| 1942 |
if (!session.shift || !session.cashier || !session.kassa) {
|
| 1943 |
alert('Смена не активна. Начните смену, чтобы проводить продажи.');
|
|
|
|
| 2325 |
<div class="modal-header"><h5 class="modal-title">Оприходование товара</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
| 2326 |
<form action="{{ url_for('stock_in') }}" method="POST">
|
| 2327 |
<div class="modal-body">
|
| 2328 |
+
<div class="mb-3">
|
| 2329 |
+
<label class="form-label">Поиск по штрих-коду</label>
|
| 2330 |
+
<div class="input-group">
|
| 2331 |
+
<input type="text" id="stockin-barcode" class="form-control barcode-input" placeholder="Отсканируйте или введите">
|
| 2332 |
+
<button type="button" class="btn btn-outline-secondary scan-modal-btn"><i class="fas fa-barcode"></i></button>
|
| 2333 |
+
</div>
|
| 2334 |
+
</div>
|
| 2335 |
+
<div id="modal-scanner-stockin" class="mb-2" style="display:none;"></div>
|
| 2336 |
<div class="mb-3">
|
| 2337 |
<label for="stockin-product" class="form-label">Товар</label>
|
| 2338 |
<select id="stockin-product" name="product_id" class="form-select" required>
|
|
|
|
| 2381 |
document.querySelectorAll('.scan-modal-btn').forEach(btn => {
|
| 2382 |
btn.addEventListener('click', e => {
|
| 2383 |
const form = e.target.closest('form');
|
| 2384 |
+
if(!form) {
|
| 2385 |
+
const modal = e.target.closest('.modal-content');
|
| 2386 |
+
const scannerContainer = modal.querySelector('[id^="modal-scanner-"]');
|
| 2387 |
+
const barcodeInput = modal.querySelector('.barcode-input');
|
| 2388 |
+
toggleScanner(scannerContainer, barcodeInput);
|
| 2389 |
+
} else {
|
| 2390 |
+
const scannerContainer = form.querySelector('[id^="modal-scanner-"]');
|
| 2391 |
+
const barcodeInput = form.querySelector('.barcode-input');
|
| 2392 |
+
toggleScanner(scannerContainer, barcodeInput);
|
| 2393 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2394 |
});
|
| 2395 |
});
|
| 2396 |
+
|
| 2397 |
+
function toggleScanner(scannerContainer, barcodeInput) {
|
| 2398 |
+
if (currentScanner) {
|
| 2399 |
+
try { currentScanner.stop(); } catch(e) {}
|
| 2400 |
+
currentScanner = null;
|
| 2401 |
+
if(currentScannerContainer) currentScannerContainer.style.display = 'none';
|
| 2402 |
+
return;
|
| 2403 |
+
}
|
| 2404 |
+
scannerContainer.style.display = 'block';
|
| 2405 |
+
currentScannerContainer = scannerContainer;
|
| 2406 |
+
const scannerId = scannerContainer.id + '-reader';
|
| 2407 |
+
if(!document.getElementById(scannerId)) scannerContainer.innerHTML = `<div id="${scannerId}" style="width: 100%;"></div>`;
|
| 2408 |
+
|
| 2409 |
+
const html5QrCode = new Html5Qrcode(scannerId);
|
| 2410 |
+
currentScanner = html5QrCode;
|
| 2411 |
+
const onScanSuccess = (decodedText, decodedResult) => {
|
| 2412 |
+
barcodeInput.value = decodedText;
|
| 2413 |
+
barcodeInput.dispatchEvent(new Event('input'));
|
| 2414 |
+
try { html5QrCode.stop(); } catch(e) {}
|
| 2415 |
+
currentScanner = null;
|
| 2416 |
+
scannerContainer.style.display = 'none';
|
| 2417 |
+
};
|
| 2418 |
+
html5QrCode.start({ facingMode: "environment" }, { fps: 10, qrbox: { width: 250, height: 250 } }, onScanSuccess);
|
| 2419 |
+
}
|
| 2420 |
|
| 2421 |
document.querySelectorAll('.modal').forEach(modal => {
|
| 2422 |
modal.addEventListener('hidden.bs.modal', () => {
|
|
|
|
| 2442 |
const formData = new FormData();
|
| 2443 |
formData.append('image', file);
|
| 2444 |
|
| 2445 |
+
preview.src = "data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
|
| 2446 |
|
| 2447 |
fetch("{{ url_for('upload_image') }}", {
|
| 2448 |
method: 'POST',
|
|
|
|
| 2518 |
const inventoryData = JSON.parse('{{ inventory|tojson|safe }}');
|
| 2519 |
const productSelect = document.getElementById('stockin-product');
|
| 2520 |
const variantSelect = document.getElementById('stockin-variant');
|
| 2521 |
+
const stockinBarcode = document.getElementById('stockin-barcode');
|
| 2522 |
+
|
| 2523 |
+
if (stockinBarcode) {
|
| 2524 |
+
stockinBarcode.addEventListener('input', (e) => {
|
| 2525 |
+
const term = e.target.value.trim().toLowerCase();
|
| 2526 |
+
const product = inventoryData.find(p => (p.barcode || '').toLowerCase() === term);
|
| 2527 |
+
if (product) {
|
| 2528 |
+
productSelect.value = product.id;
|
| 2529 |
+
productSelect.dispatchEvent(new Event('change'));
|
| 2530 |
+
}
|
| 2531 |
+
});
|
| 2532 |
+
}
|
| 2533 |
|
| 2534 |
productSelect.addEventListener('change', () => {
|
| 2535 |
const productId = productSelect.value;
|
|
|
|
| 2694 |
|
| 2695 |
document.getElementById('save-trans-btn').addEventListener('click', () => {
|
| 2696 |
const form = document.getElementById('edit-trans-form');
|
| 2697 |
+
const items_update =[];
|
| 2698 |
form.querySelectorAll('tbody tr').forEach(row => {
|
| 2699 |
items_update.push({
|
| 2700 |
id: row.dataset.itemId,
|