Update app.py
Browse files
app.py
CHANGED
|
@@ -234,8 +234,16 @@ def sales_screen():
|
|
| 234 |
active_inventory.append(p)
|
| 235 |
|
| 236 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 238 |
-
return render_template_string(html, inventory=active_inventory, users=users, kassas=kassas)
|
| 239 |
|
| 240 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 241 |
@admin_required
|
|
@@ -717,13 +725,16 @@ def product_roi_report():
|
|
| 717 |
if t['type'] == 'sale':
|
| 718 |
total_qty_sold += item['quantity']
|
| 719 |
elif t['type'] == 'return':
|
| 720 |
-
total_qty_sold
|
| 721 |
|
| 722 |
current_stock = to_decimal(str(variant.get('stock', 0)))
|
| 723 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 724 |
|
| 725 |
inventory_value = current_stock * cost_price
|
| 726 |
-
|
|
|
|
|
|
|
|
|
|
| 727 |
payback = total_revenue - total_investment
|
| 728 |
|
| 729 |
product_stats.append({
|
|
@@ -972,6 +983,8 @@ def return_transaction(transaction_id):
|
|
| 972 |
return_items = []
|
| 973 |
inventory_updates = {}
|
| 974 |
for item in original_transaction['items']:
|
|
|
|
|
|
|
| 975 |
return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
|
| 976 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 977 |
if product:
|
|
@@ -992,7 +1005,7 @@ def return_transaction(transaction_id):
|
|
| 992 |
'kassa_id': original_transaction['kassa_id'],
|
| 993 |
'kassa_name': original_transaction['kassa_name'],
|
| 994 |
'items': return_items,
|
| 995 |
-
'total_amount': str(-total_amount),
|
| 996 |
'payment_method': original_transaction['payment_method']
|
| 997 |
}
|
| 998 |
transactions.append(return_transaction)
|
|
@@ -1016,7 +1029,7 @@ def return_transaction(transaction_id):
|
|
| 1016 |
for i, k in enumerate(kassas):
|
| 1017 |
if k['id'] == original_transaction['kassa_id']:
|
| 1018 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1019 |
-
kassas[i]['balance'] = str(current_balance - total_amount)
|
| 1020 |
kassas[i].setdefault('history', []).append({
|
| 1021 |
'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
|
| 1022 |
'transaction_id': return_transaction['id']
|
|
@@ -1253,20 +1266,34 @@ SALES_SCREEN_CONTENT = """
|
|
| 1253 |
<button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
|
| 1254 |
</div>
|
| 1255 |
<input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
<
|
| 1262 |
-
{
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
{
|
| 1269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
</div>
|
| 1271 |
</div>
|
| 1272 |
{% endfor %}
|
|
@@ -1301,7 +1328,7 @@ SALES_SCREEN_SCRIPTS = """
|
|
| 1301 |
<script>
|
| 1302 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1303 |
const cart = {};
|
| 1304 |
-
const productGrid = document.getElementById('product-
|
| 1305 |
const cartItemsEl = document.getElementById('cart-items');
|
| 1306 |
const cartTotalEl = document.getElementById('cart-total');
|
| 1307 |
let audioCtx;
|
|
@@ -1464,11 +1491,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1464 |
|
| 1465 |
document.getElementById('product-search').addEventListener('input', e => {
|
| 1466 |
const term = e.target.value.toLowerCase();
|
| 1467 |
-
document.querySelectorAll('.product-card')
|
|
|
|
|
|
|
| 1468 |
const productName = card.querySelector('.card-title').textContent.toLowerCase();
|
| 1469 |
-
const
|
|
|
|
| 1470 |
card.style.display = show ? '' : 'none';
|
| 1471 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1472 |
});
|
| 1473 |
|
| 1474 |
const completeSale = (paymentMethod) => {
|
|
@@ -1511,7 +1557,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1511 |
if (phone && receiptUrl) {
|
| 1512 |
const fullPhone = '996' + phone;
|
| 1513 |
const message = encodeURIComponent(`Ваш чек: ${receiptUrl}`);
|
| 1514 |
-
window.open(`https
|
| 1515 |
} else {
|
| 1516 |
alert('Введите номер телефона.');
|
| 1517 |
}
|
|
@@ -1566,6 +1612,7 @@ INVENTORY_CONTENT = """
|
|
| 1566 |
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal"><i class="fas fa-plus me-2"></i>Добавить товар</button>
|
| 1567 |
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#stockInModal"><i class="fas fa-truck-loading me-2"></i>Оприходовать</button>
|
| 1568 |
</div>
|
|
|
|
| 1569 |
|
| 1570 |
<div class="accordion" id="inventoryAccordion">
|
| 1571 |
{% for p in inventory %}
|
|
@@ -1814,6 +1861,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1814 |
}
|
| 1815 |
}
|
| 1816 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1817 |
});
|
| 1818 |
</script>
|
| 1819 |
"""
|
|
|
|
| 234 |
active_inventory.append(p)
|
| 235 |
|
| 236 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
| 237 |
+
|
| 238 |
+
grouped_inventory = defaultdict(list)
|
| 239 |
+
for p in active_inventory:
|
| 240 |
+
first_letter = p.get('name', '#')[0].upper()
|
| 241 |
+
grouped_inventory[first_letter].append(p)
|
| 242 |
+
|
| 243 |
+
sorted_grouped_inventory = sorted(grouped_inventory.items())
|
| 244 |
+
|
| 245 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 246 |
+
return render_template_string(html, inventory=active_inventory, users=users, kassas=kassas, grouped_inventory=sorted_grouped_inventory)
|
| 247 |
|
| 248 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 249 |
@admin_required
|
|
|
|
| 725 |
if t['type'] == 'sale':
|
| 726 |
total_qty_sold += item['quantity']
|
| 727 |
elif t['type'] == 'return':
|
| 728 |
+
total_qty_sold -= item['quantity'] # Corrected to subtract for returns
|
| 729 |
|
| 730 |
current_stock = to_decimal(str(variant.get('stock', 0)))
|
| 731 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 732 |
|
| 733 |
inventory_value = current_stock * cost_price
|
| 734 |
+
|
| 735 |
+
# For simplicity, calculate total investment as historical COGS + current inventory value.
|
| 736 |
+
# This is a basic model, more advanced models might consider purchase dates, etc.
|
| 737 |
+
total_investment = total_cogs + inventory_value
|
| 738 |
payback = total_revenue - total_investment
|
| 739 |
|
| 740 |
product_stats.append({
|
|
|
|
| 983 |
return_items = []
|
| 984 |
inventory_updates = {}
|
| 985 |
for item in original_transaction['items']:
|
| 986 |
+
# For a return, quantity becomes negative, and total becomes negative.
|
| 987 |
+
# This will reverse the effect on stock and total amount.
|
| 988 |
return_items.append({**item, 'quantity': -item['quantity'], 'total': str(-to_decimal(item['total']))})
|
| 989 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 990 |
if product:
|
|
|
|
| 1005 |
'kassa_id': original_transaction['kassa_id'],
|
| 1006 |
'kassa_name': original_transaction['kassa_name'],
|
| 1007 |
'items': return_items,
|
| 1008 |
+
'total_amount': str(-total_amount), # Total amount for return is negative
|
| 1009 |
'payment_method': original_transaction['payment_method']
|
| 1010 |
}
|
| 1011 |
transactions.append(return_transaction)
|
|
|
|
| 1029 |
for i, k in enumerate(kassas):
|
| 1030 |
if k['id'] == original_transaction['kassa_id']:
|
| 1031 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1032 |
+
kassas[i]['balance'] = str(current_balance - total_amount) # Subtract total amount for cash return
|
| 1033 |
kassas[i].setdefault('history', []).append({
|
| 1034 |
'type': 'return', 'amount': str(-total_amount), 'timestamp': now_iso,
|
| 1035 |
'transaction_id': return_transaction['id']
|
|
|
|
| 1266 |
<button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
|
| 1267 |
</div>
|
| 1268 |
<input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
|
| 1269 |
+
|
| 1270 |
+
<div id="product-accordion" class="accordion">
|
| 1271 |
+
{% for letter, products in grouped_inventory %}
|
| 1272 |
+
<div class="accordion-item">
|
| 1273 |
+
<h2 class="accordion-header" id="heading-{{ letter }}">
|
| 1274 |
+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ letter }}" aria-expanded="false" aria-controls="collapse-{{ letter }}">
|
| 1275 |
+
{{ letter }}
|
| 1276 |
+
</button>
|
| 1277 |
+
</h2>
|
| 1278 |
+
<div id="collapse-{{ letter }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ letter }}" data-bs-parent="#product-accordion">
|
| 1279 |
+
<div class="accordion-body d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
|
| 1280 |
+
{% for p in products %}
|
| 1281 |
+
<div class="card text-center product-card" data-barcode="{{ p.barcode }}">
|
| 1282 |
+
<div class="card-body p-2">
|
| 1283 |
+
<h6 class="card-title small mb-1">{{ p.name }}</h6>
|
| 1284 |
+
<p class="card-text fw-bold mb-0">
|
| 1285 |
+
{% if p.variants|length > 1 %}
|
| 1286 |
+
от {{ format_currency_py(p.variants|map(attribute='price')|min) }} с
|
| 1287 |
+
{% elif p.variants|length == 1 %}
|
| 1288 |
+
{{ format_currency_py(p.variants[0].price) }} с
|
| 1289 |
+
{% else %}
|
| 1290 |
+
Нет в наличии
|
| 1291 |
+
{% endif %}
|
| 1292 |
+
</p>
|
| 1293 |
+
</div>
|
| 1294 |
+
</div>
|
| 1295 |
+
{% endfor %}
|
| 1296 |
+
</div>
|
| 1297 |
</div>
|
| 1298 |
</div>
|
| 1299 |
{% endfor %}
|
|
|
|
| 1328 |
<script>
|
| 1329 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1330 |
const cart = {};
|
| 1331 |
+
const productGrid = document.getElementById('product-accordion');
|
| 1332 |
const cartItemsEl = document.getElementById('cart-items');
|
| 1333 |
const cartTotalEl = document.getElementById('cart-total');
|
| 1334 |
let audioCtx;
|
|
|
|
| 1491 |
|
| 1492 |
document.getElementById('product-search').addEventListener('input', e => {
|
| 1493 |
const term = e.target.value.toLowerCase();
|
| 1494 |
+
const productCards = document.querySelectorAll('#product-accordion .product-card');
|
| 1495 |
+
|
| 1496 |
+
productCards.forEach(card => {
|
| 1497 |
const productName = card.querySelector('.card-title').textContent.toLowerCase();
|
| 1498 |
+
const barcode = card.dataset.barcode.toLowerCase();
|
| 1499 |
+
const show = productName.includes(term) || barcode.includes(term);
|
| 1500 |
card.style.display = show ? '' : 'none';
|
| 1501 |
});
|
| 1502 |
+
|
| 1503 |
+
document.querySelectorAll('#product-accordion .accordion-item').forEach(accordionItem => {
|
| 1504 |
+
const collapseElement = accordionItem.querySelector('.accordion-collapse');
|
| 1505 |
+
const matchingCardsInGroup = accordionItem.querySelectorAll('.product-card:not([style*="display: none"])');
|
| 1506 |
+
const bsCollapse = bootstrap.Collapse.getOrCreateInstance(collapseElement, { toggle: false });
|
| 1507 |
+
|
| 1508 |
+
if (term === '') {
|
| 1509 |
+
bsCollapse.hide();
|
| 1510 |
+
} else {
|
| 1511 |
+
if (matchingCardsInGroup.length > 0) {
|
| 1512 |
+
bsCollapse.show();
|
| 1513 |
+
} else {
|
| 1514 |
+
bsCollapse.hide();
|
| 1515 |
+
}
|
| 1516 |
+
}
|
| 1517 |
+
});
|
| 1518 |
});
|
| 1519 |
|
| 1520 |
const completeSale = (paymentMethod) => {
|
|
|
|
| 1557 |
if (phone && receiptUrl) {
|
| 1558 |
const fullPhone = '996' + phone;
|
| 1559 |
const message = encodeURIComponent(`Ваш чек: ${receiptUrl}`);
|
| 1560 |
+
window.open(`https://wa.me/${fullPhone}?text=${message}`, '_blank');
|
| 1561 |
} else {
|
| 1562 |
alert('Введите номер телефона.');
|
| 1563 |
}
|
|
|
|
| 1612 |
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal"><i class="fas fa-plus me-2"></i>Добавить товар</button>
|
| 1613 |
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#stockInModal"><i class="fas fa-truck-loading me-2"></i>Оприходовать</button>
|
| 1614 |
</div>
|
| 1615 |
+
<input type="text" id="inventory-search" class="form-control mb-3" placeholder="Поиск по названию, варианту или штрих-коду...">
|
| 1616 |
|
| 1617 |
<div class="accordion" id="inventoryAccordion">
|
| 1618 |
{% for p in inventory %}
|
|
|
|
| 1861 |
}
|
| 1862 |
}
|
| 1863 |
});
|
| 1864 |
+
|
| 1865 |
+
document.getElementById('inventory-search').addEventListener('input', e => {
|
| 1866 |
+
const term = e.target.value.toLowerCase();
|
| 1867 |
+
document.querySelectorAll('#inventoryAccordion .accordion-item').forEach(item => {
|
| 1868 |
+
const productName = item.querySelector('.accordion-button strong').textContent.toLowerCase();
|
| 1869 |
+
const barcode = item.querySelector('.accordion-button small').textContent.toLowerCase();
|
| 1870 |
+
|
| 1871 |
+
const variantNameElements = item.querySelectorAll('.accordion-body table tbody tr td:first-child');
|
| 1872 |
+
const variantMatch = Array.from(variantNameElements).some(td => td.textContent.toLowerCase().includes(term));
|
| 1873 |
+
|
| 1874 |
+
const show = productName.includes(term) || barcode.includes(term) || variantMatch;
|
| 1875 |
+
item.style.display = show ? '' : 'none';
|
| 1876 |
+
});
|
| 1877 |
+
});
|
| 1878 |
});
|
| 1879 |
</script>
|
| 1880 |
"""
|