Update app.py
Browse files
app.py
CHANGED
|
@@ -95,7 +95,7 @@ def load_json_data(file_key):
|
|
| 95 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 96 |
return json.load(f)
|
| 97 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 98 |
-
return[]
|
| 99 |
|
| 100 |
def save_json_data(file_key, data):
|
| 101 |
filepath, lock = DATA_FILES[file_key]
|
|
@@ -142,7 +142,7 @@ def periodic_backup():
|
|
| 142 |
for key in DATA_FILES.keys():
|
| 143 |
upload_db_to_hf(key)
|
| 144 |
except Exception as e:
|
| 145 |
-
logging.error(f"Error scheduled backup: {e}", exc_info=True)
|
| 146 |
|
| 147 |
def find_item_by_field(data, field, value):
|
| 148 |
for item in data:
|
|
@@ -162,7 +162,7 @@ def format_currency_py(value):
|
|
| 162 |
return "0"
|
| 163 |
|
| 164 |
def generate_receipt_html(transaction):
|
| 165 |
-
has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items',[]))
|
| 166 |
delivery_cost = to_decimal(transaction.get('delivery_cost', '0'))
|
| 167 |
note = transaction.get('note', '')
|
| 168 |
|
|
@@ -187,9 +187,7 @@ def generate_receipt_html(transaction):
|
|
| 187 |
total_colspan = 4
|
| 188 |
|
| 189 |
items_html = ""
|
| 190 |
-
total_qty = 0
|
| 191 |
for i, item in enumerate(transaction['items']):
|
| 192 |
-
total_qty += int(item.get('quantity', 0))
|
| 193 |
discount_cell = f"""<td style="text-align: right;">{format_currency_py(item.get('discount_per_item', '0'))}</td>""" if has_discounts else ""
|
| 194 |
items_html += f"""
|
| 195 |
<tr>
|
|
@@ -204,13 +202,7 @@ def generate_receipt_html(transaction):
|
|
| 204 |
|
| 205 |
total_amount_from_db = to_decimal(transaction['total_amount'])
|
| 206 |
|
| 207 |
-
totals_html =
|
| 208 |
-
<tr>
|
| 209 |
-
<td colspan="{total_colspan}" style="text-align: right;">Общее кол-во:</td>
|
| 210 |
-
<td style="text-align: right;">{total_qty} шт.</td>
|
| 211 |
-
</tr>
|
| 212 |
-
"""
|
| 213 |
-
|
| 214 |
if delivery_cost > 0:
|
| 215 |
subtotal = total_amount_from_db - delivery_cost
|
| 216 |
totals_html += f"""
|
|
@@ -258,20 +250,20 @@ def generate_receipt_html(transaction):
|
|
| 258 |
<title>Накладная {transaction['id'][:8]}</title>
|
| 259 |
<style>
|
| 260 |
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 10px; background-color: #f4f4f4; color: #333; }}
|
| 261 |
-
.invoice-box {{ max-width:
|
| 262 |
.header {{ text-align: center; margin-bottom: 20px; }}
|
| 263 |
.header h1 {{ margin: 0; font-size: 22px; font-weight: 600; }}
|
| 264 |
.header p {{ margin: 2px 0; font-size: 14px; }}
|
| 265 |
.details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
|
| 266 |
table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
|
| 267 |
-
table th
|
| 268 |
-
table
|
| 269 |
table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
|
| 270 |
.footer-info {{ font-size: 14px; margin-top: 20px; }}
|
| 271 |
.print-hide {{ display: block; }}
|
| 272 |
@media print {{
|
| 273 |
body {{ margin: 0; padding: 0; background-color: #fff; }}
|
| 274 |
-
.invoice-box {{ box-shadow: none; border: none; margin: 0; padding: 0;
|
| 275 |
.print-hide {{ display: none; }}
|
| 276 |
}}
|
| 277 |
@media screen and (max-width: 600px) {{
|
|
@@ -295,13 +287,13 @@ def generate_receipt_html(transaction):
|
|
| 295 |
<h1>Товарная накладная № {transaction['id'][:8]}</h1>
|
| 296 |
<p>от {datetime.fromisoformat(transaction['timestamp']).strftime('%d.%m.%Y %H:%M')}</p>
|
| 297 |
</div>
|
| 298 |
-
<table
|
| 299 |
<thead>
|
| 300 |
<tr>{table_headers}</tr>
|
| 301 |
</thead>
|
| 302 |
<tbody>{items_html}</tbody>
|
| 303 |
</table>
|
| 304 |
-
<table style="margin-top: 20px;
|
| 305 |
{totals_html}
|
| 306 |
</table>
|
| 307 |
{note_html}
|
|
@@ -368,15 +360,22 @@ def sales_screen():
|
|
| 368 |
transactions = load_json_data('transactions')
|
| 369 |
edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
|
| 370 |
|
| 371 |
-
active_inventory =[]
|
| 372 |
for p in inventory:
|
| 373 |
-
if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants',[])):
|
| 374 |
active_inventory.append(p)
|
| 375 |
|
| 376 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
| 377 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 379 |
-
return render_template_string(html, inventory=active_inventory, kassas=kassas, edit_tx=edit_tx)
|
| 380 |
|
| 381 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 382 |
def inventory_management():
|
|
@@ -396,7 +395,7 @@ def inventory_management():
|
|
| 396 |
flash(f"Товар со штрих-кодом {barcode} уже существует.", "warning")
|
| 397 |
return redirect(url_for('inventory_management'))
|
| 398 |
|
| 399 |
-
variants =[]
|
| 400 |
variant_names = request.form.getlist('variant_name[]')
|
| 401 |
variant_prices_regular = request.form.getlist('variant_price_regular[]')
|
| 402 |
variant_prices_min = request.form.getlist('variant_price_min[]')
|
|
@@ -462,7 +461,7 @@ def inventory_management():
|
|
| 462 |
|
| 463 |
for product in inventory_list:
|
| 464 |
if isinstance(product, dict) and 'variants' in product:
|
| 465 |
-
for variant in product.get('variants',[]):
|
| 466 |
stock = variant.get('stock', 0)
|
| 467 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 468 |
price = to_decimal(variant.get('price_regular', variant.get('price', '0')))
|
|
@@ -504,7 +503,7 @@ def update_prices():
|
|
| 504 |
|
| 505 |
for p in inventory:
|
| 506 |
updated = False
|
| 507 |
-
for v in p.get('variants',[]):
|
| 508 |
if v.get('id') in updates:
|
| 509 |
v['price_regular'] = str(to_decimal(updates[v['id']][0]))
|
| 510 |
v['price_min'] = str(to_decimal(updates[v['id']][1]))
|
|
@@ -542,7 +541,7 @@ def edit_product(product_id):
|
|
| 542 |
inventory[i]['name'] = name
|
| 543 |
inventory[i]['barcode'] = barcode
|
| 544 |
|
| 545 |
-
new_variants =[]
|
| 546 |
variant_ids = request.form.getlist('variant_id[]')
|
| 547 |
variant_names = request.form.getlist('variant_name[]')
|
| 548 |
variant_prices_regular = request.form.getlist('variant_price_regular[]')
|
|
@@ -589,7 +588,7 @@ def edit_product(product_id):
|
|
| 589 |
def delete_product(product_id):
|
| 590 |
inventory = load_json_data('inventory')
|
| 591 |
initial_len = len(inventory)
|
| 592 |
-
inventory =[p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
|
| 593 |
if len(inventory) < initial_len:
|
| 594 |
save_json_data('inventory', inventory)
|
| 595 |
upload_db_to_hf('inventory')
|
|
@@ -621,7 +620,7 @@ def stock_in():
|
|
| 621 |
|
| 622 |
variant_found = False
|
| 623 |
variant_name_for_log = ""
|
| 624 |
-
for i, variant in enumerate(product.get('variants',[])):
|
| 625 |
if variant.get('id') == variant_id:
|
| 626 |
variant_name_for_log = variant.get('option_value', '')
|
| 627 |
|
|
@@ -705,7 +704,7 @@ def get_product_by_barcode(barcode):
|
|
| 705 |
inventory = load_json_data('inventory')
|
| 706 |
product = find_item_by_field(inventory, 'barcode', barcode)
|
| 707 |
if product:
|
| 708 |
-
active_variants =[v for v in product.get('variants', []) if v.get('stock', 0) > 0]
|
| 709 |
if active_variants:
|
| 710 |
product_copy = product.copy()
|
| 711 |
product_copy['variants'] = active_variants
|
|
@@ -735,11 +734,11 @@ def complete_sale():
|
|
| 735 |
if not original_tx:
|
| 736 |
return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
|
| 737 |
|
| 738 |
-
for item in original_tx.get('items',[]):
|
| 739 |
if not item.get('is_custom'):
|
| 740 |
for p in inventory:
|
| 741 |
if p.get('id') == item.get('product_id'):
|
| 742 |
-
for v in p.get('variants',[]):
|
| 743 |
if v.get('id') == item.get('variant_id'):
|
| 744 |
v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
|
| 745 |
break
|
|
@@ -751,7 +750,7 @@ def complete_sale():
|
|
| 751 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 752 |
amount = to_decimal(original_tx.get('total_amount', '0'))
|
| 753 |
k['balance'] = str(current_balance - amount)
|
| 754 |
-
k.setdefault('history',[]).append({
|
| 755 |
'type': 'correction_revert',
|
| 756 |
'amount': str(-amount),
|
| 757 |
'timestamp': get_current_time().isoformat(),
|
|
@@ -775,7 +774,7 @@ def complete_sale():
|
|
| 775 |
if not user or not kassa:
|
| 776 |
return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
|
| 777 |
|
| 778 |
-
sale_items =[]
|
| 779 |
items_total = Decimal('0')
|
| 780 |
inventory_updates = {}
|
| 781 |
|
|
@@ -804,7 +803,7 @@ def complete_sale():
|
|
| 804 |
if not product:
|
| 805 |
return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
|
| 806 |
|
| 807 |
-
variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
|
| 808 |
if not variant:
|
| 809 |
return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
|
| 810 |
|
|
@@ -878,7 +877,7 @@ def complete_sale():
|
|
| 878 |
for variant_id, update_info in inventory_updates.items():
|
| 879 |
for p in inventory:
|
| 880 |
if p.get('id') == update_info['product_id']:
|
| 881 |
-
for v in p.get('variants',[]):
|
| 882 |
if v.get('id') == variant_id:
|
| 883 |
v['stock'] = update_info['new_stock']
|
| 884 |
p['timestamp_updated'] = now_iso
|
|
@@ -945,13 +944,13 @@ def transaction_history():
|
|
| 945 |
transactions = load_json_data('transactions')
|
| 946 |
kassas = load_json_data('kassas')
|
| 947 |
|
| 948 |
-
filtered_transactions =[
|
| 949 |
t for t in transactions
|
| 950 |
if datetime.fromisoformat(t['timestamp']).date() == selected_date
|
| 951 |
]
|
| 952 |
|
| 953 |
if selected_kassa_id:
|
| 954 |
-
filtered_transactions =[
|
| 955 |
t for t in filtered_transactions
|
| 956 |
if t.get('kassa_id') == selected_kassa_id
|
| 957 |
]
|
|
@@ -961,7 +960,7 @@ def transaction_history():
|
|
| 961 |
total_quantity_sold = 0
|
| 962 |
for t in filtered_transactions:
|
| 963 |
if t.get('type') == 'sale':
|
| 964 |
-
for item in t.get('items',[]):
|
| 965 |
total_quantity_sold += int(item.get('quantity', 0))
|
| 966 |
|
| 967 |
filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
@@ -974,7 +973,7 @@ def transaction_history():
|
|
| 974 |
def edit_transaction(transaction_id):
|
| 975 |
try:
|
| 976 |
data = request.get_json()
|
| 977 |
-
items_update = data.get('items',[])
|
| 978 |
|
| 979 |
transactions = load_json_data('transactions')
|
| 980 |
kassas = load_json_data('kassas')
|
|
@@ -992,7 +991,7 @@ def edit_transaction(transaction_id):
|
|
| 992 |
old_total_amount = to_decimal(original_transaction['total_amount'])
|
| 993 |
|
| 994 |
new_items_total = Decimal('0')
|
| 995 |
-
updated_items =[]
|
| 996 |
|
| 997 |
for item in original_transaction['items']:
|
| 998 |
item_id = item.get('variant_id') or item.get('product_id')
|
|
@@ -1014,7 +1013,7 @@ def edit_transaction(transaction_id):
|
|
| 1014 |
transactions[transaction_index]['total_amount'] = str(new_total_amount)
|
| 1015 |
|
| 1016 |
if 'edits' not in transactions[transaction_index]:
|
| 1017 |
-
transactions[transaction_index]['edits'] =[]
|
| 1018 |
|
| 1019 |
transactions[transaction_index]['edits'].append({
|
| 1020 |
'timestamp': get_current_time().isoformat(),
|
|
@@ -1031,7 +1030,7 @@ def edit_transaction(transaction_id):
|
|
| 1031 |
for i, k in enumerate(kassas):
|
| 1032 |
if k.get('id') == kassa_id:
|
| 1033 |
k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
|
| 1034 |
-
k.setdefault('history',[]).append({
|
| 1035 |
'type': 'correction',
|
| 1036 |
'amount': str(amount_diff),
|
| 1037 |
'timestamp': get_current_time().isoformat(),
|
|
@@ -1062,14 +1061,14 @@ def delete_transaction(transaction_id):
|
|
| 1062 |
flash("Транзакция не найдена.", "danger")
|
| 1063 |
return redirect(url_for('transaction_history'))
|
| 1064 |
|
| 1065 |
-
for item in transaction_to_delete.get('items',[]):
|
| 1066 |
if item.get('is_custom'):
|
| 1067 |
continue
|
| 1068 |
|
| 1069 |
product = find_item_by_field(inventory, 'id', item.get('product_id'))
|
| 1070 |
if not product: continue
|
| 1071 |
|
| 1072 |
-
variant = find_item_by_field(product.get('variants',[]), 'id', item.get('variant_id'))
|
| 1073 |
if not variant: continue
|
| 1074 |
|
| 1075 |
quantity_change = item.get('quantity', 0)
|
|
@@ -1087,7 +1086,7 @@ def delete_transaction(transaction_id):
|
|
| 1087 |
|
| 1088 |
kassa['balance'] = str(current_balance - amount_change)
|
| 1089 |
|
| 1090 |
-
kassa.setdefault('history',[]).append({
|
| 1091 |
'type': 'deletion',
|
| 1092 |
'amount': str(-amount_change),
|
| 1093 |
'timestamp': get_current_time().isoformat(),
|
|
@@ -1131,20 +1130,20 @@ def reports():
|
|
| 1131 |
personal_expenses = load_json_data('personal_expenses')
|
| 1132 |
users = load_json_data('users')
|
| 1133 |
|
| 1134 |
-
filtered_transactions =[
|
| 1135 |
t for t in transactions
|
| 1136 |
if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
|
| 1137 |
]
|
| 1138 |
-
filtered_expenses =[
|
| 1139 |
e for e in expenses
|
| 1140 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 1141 |
]
|
| 1142 |
-
filtered_personal_expenses =[
|
| 1143 |
e for e in personal_expenses
|
| 1144 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 1145 |
]
|
| 1146 |
|
| 1147 |
-
return_transactions =[t for t in filtered_transactions if t.get('type') == 'return']
|
| 1148 |
total_returns_amount = sum(to_decimal(t['total_amount']) for t in return_transactions)
|
| 1149 |
total_returns_count = len(return_transactions)
|
| 1150 |
|
|
@@ -1223,7 +1222,7 @@ def employee_report():
|
|
| 1223 |
end_date = (datetime.strptime(end_date_str, '%Y-%m-%d') + timedelta(days=1)).replace(tzinfo=ALMATY_TZ)
|
| 1224 |
|
| 1225 |
transactions = load_json_data('transactions')
|
| 1226 |
-
employee_transactions =[]
|
| 1227 |
totals = {'sales': Decimal(0), 'returns': Decimal(0), 'net': Decimal(0)}
|
| 1228 |
|
| 1229 |
if user_id:
|
|
@@ -1255,17 +1254,17 @@ def item_movement_report():
|
|
| 1255 |
product_id = request.args.get('product_id', '')
|
| 1256 |
variant_id = request.args.get('variant_id', '')
|
| 1257 |
|
| 1258 |
-
movements =[]
|
| 1259 |
selected_product = None
|
| 1260 |
selected_variant = None
|
| 1261 |
|
| 1262 |
if product_id:
|
| 1263 |
selected_product = find_item_by_field(inventory, 'id', product_id)
|
| 1264 |
if selected_product and variant_id:
|
| 1265 |
-
selected_variant = find_item_by_field(selected_product.get('variants',[]), 'id', variant_id)
|
| 1266 |
|
| 1267 |
for t in transactions:
|
| 1268 |
-
for item in t.get('items',[]):
|
| 1269 |
if item.get('product_id') == product_id and (not variant_id or item.get('variant_id') == variant_id):
|
| 1270 |
movements.append({
|
| 1271 |
'timestamp': t['timestamp'],
|
|
@@ -1300,10 +1299,10 @@ def product_roi_report():
|
|
| 1300 |
inventory = load_json_data('inventory')
|
| 1301 |
transactions = load_json_data('transactions')
|
| 1302 |
|
| 1303 |
-
product_stats =[]
|
| 1304 |
|
| 1305 |
for product in inventory:
|
| 1306 |
-
for variant in product.get('variants',[]):
|
| 1307 |
total_revenue = Decimal('0')
|
| 1308 |
total_cogs = Decimal('0')
|
| 1309 |
total_qty_sold = 0
|
|
@@ -1398,7 +1397,7 @@ def manage_user():
|
|
| 1398 |
elif action == 'delete':
|
| 1399 |
user_id = request.form.get('id')
|
| 1400 |
initial_len = len(users)
|
| 1401 |
-
users =[u for u in users if u.get('id') != user_id]
|
| 1402 |
if len(users) < initial_len:
|
| 1403 |
flash("Кассир удален.", "success")
|
| 1404 |
else:
|
|
@@ -1422,7 +1421,7 @@ def manage_kassa():
|
|
| 1422 |
'id': uuid.uuid4().hex,
|
| 1423 |
'name': name,
|
| 1424 |
'balance': str(balance),
|
| 1425 |
-
'history':[]
|
| 1426 |
}
|
| 1427 |
if balance > 0:
|
| 1428 |
new_kassa['history'].append({
|
|
@@ -1439,7 +1438,7 @@ def manage_kassa():
|
|
| 1439 |
elif action == 'delete':
|
| 1440 |
kassa_id = request.form.get('id')
|
| 1441 |
initial_len = len(kassas)
|
| 1442 |
-
kassas =[k for k in kassas if k.get('id') != kassa_id]
|
| 1443 |
if len(kassas) < initial_len:
|
| 1444 |
flash("Касса удалена.", "success")
|
| 1445 |
else:
|
|
@@ -1524,7 +1523,7 @@ def manage_expense():
|
|
| 1524 |
def delete_expense(expense_id):
|
| 1525 |
expenses = load_json_data('expenses')
|
| 1526 |
initial_len = len(expenses)
|
| 1527 |
-
expenses =[e for e in expenses if e.get('id') != expense_id]
|
| 1528 |
if len(expenses) < initial_len:
|
| 1529 |
save_json_data('expenses', expenses)
|
| 1530 |
upload_db_to_hf('expenses')
|
|
@@ -1561,7 +1560,7 @@ def manage_personal_expense():
|
|
| 1561 |
def delete_personal_expense(expense_id):
|
| 1562 |
expenses = load_json_data('personal_expenses')
|
| 1563 |
initial_len = len(expenses)
|
| 1564 |
-
expenses =[e for e in expenses if e.get('id') != expense_id]
|
| 1565 |
if len(expenses) < initial_len:
|
| 1566 |
save_json_data('personal_expenses', expenses)
|
| 1567 |
upload_db_to_hf('personal_expenses')
|
|
@@ -1583,7 +1582,7 @@ def manage_link():
|
|
| 1583 |
flash("Ссылка добавлена.", "success")
|
| 1584 |
elif action == 'delete':
|
| 1585 |
link_id = request.form.get('id')
|
| 1586 |
-
links =[l for l in links if l.get('id') != link_id]
|
| 1587 |
flash("Ссылка удалена.", "success")
|
| 1588 |
save_json_data('links', links)
|
| 1589 |
upload_db_to_hf('links')
|
|
@@ -1680,7 +1679,7 @@ def end_shift():
|
|
| 1680 |
|
| 1681 |
shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
|
| 1682 |
|
| 1683 |
-
shift_transactions =[
|
| 1684 |
t for t in transactions
|
| 1685 |
if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
|
| 1686 |
]
|
|
@@ -1711,7 +1710,7 @@ def cashier_dashboard(user_id):
|
|
| 1711 |
abort(404, "Кассир не найден")
|
| 1712 |
|
| 1713 |
transactions = load_json_data('transactions')
|
| 1714 |
-
user_transactions =[t for t in transactions if t.get('user_id') == user_id]
|
| 1715 |
user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 1716 |
|
| 1717 |
html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
|
|
@@ -1741,7 +1740,7 @@ def return_transaction(transaction_id):
|
|
| 1741 |
flash("Не указан ID кассира.", "danger")
|
| 1742 |
return redirect(url_for('cashier_login'))
|
| 1743 |
|
| 1744 |
-
returnable_items =[]
|
| 1745 |
already_returned = original_transaction.get('return_info', {}).get('returned_items', {})
|
| 1746 |
|
| 1747 |
for item in original_transaction['items']:
|
|
@@ -1762,7 +1761,7 @@ def return_transaction(transaction_id):
|
|
| 1762 |
flash("Не удалось определить кассира.", "danger")
|
| 1763 |
return redirect(url_for('cashier_login'))
|
| 1764 |
|
| 1765 |
-
return_items =[]
|
| 1766 |
total_return_amount = Decimal('0')
|
| 1767 |
inventory_updates = {}
|
| 1768 |
items_to_process = defaultdict(int)
|
|
@@ -1807,7 +1806,7 @@ def return_transaction(transaction_id):
|
|
| 1807 |
if not item.get('is_custom'):
|
| 1808 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 1809 |
if product:
|
| 1810 |
-
variant = find_item_by_field(product.get('variants',[]), 'id', variant_id)
|
| 1811 |
if variant:
|
| 1812 |
inventory_updates[variant_id] = {'product_id': item['product_id'], 'stock_change': qty_to_return}
|
| 1813 |
|
|
@@ -1854,7 +1853,7 @@ def return_transaction(transaction_id):
|
|
| 1854 |
if k.get('id') == original_transaction['kassa_id']:
|
| 1855 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1856 |
kassas[k_idx]['balance'] = str(current_balance - total_return_amount)
|
| 1857 |
-
kassas[k_idx].setdefault('history',[]).append({
|
| 1858 |
'type': 'return', 'amount': str(-total_return_amount), 'timestamp': now_iso,
|
| 1859 |
'transaction_id': return_transaction_id
|
| 1860 |
})
|
|
@@ -1884,7 +1883,7 @@ def backup_hf():
|
|
| 1884 |
@app.route('/download', methods=['GET'])
|
| 1885 |
@admin_required
|
| 1886 |
def download_hf():
|
| 1887 |
-
errors =[]
|
| 1888 |
success_count = 0
|
| 1889 |
for key in DATA_FILES.keys():
|
| 1890 |
filepath, _ = DATA_FILES[key]
|
|
@@ -1955,7 +1954,7 @@ def restore_bill(bill_id):
|
|
| 1955 |
if not bill_to_restore:
|
| 1956 |
return jsonify({'success': False, 'message': 'Bill not found'}), 404
|
| 1957 |
|
| 1958 |
-
remaining_bills =[b for b in bills if b.get('id') != bill_id]
|
| 1959 |
save_json_data('held_bills', remaining_bills)
|
| 1960 |
upload_db_to_hf('held_bills')
|
| 1961 |
return jsonify({'success': True, 'bill': bill_to_restore})
|
|
@@ -2002,7 +2001,7 @@ def manage_customer():
|
|
| 2002 |
elif action == 'delete':
|
| 2003 |
customer_id = request.form.get('id')
|
| 2004 |
initial_len = len(customers)
|
| 2005 |
-
customers =[c for c in customers if c.get('id') != customer_id]
|
| 2006 |
if len(customers) < initial_len:
|
| 2007 |
flash("Клиент удален.", "success")
|
| 2008 |
else:
|
|
@@ -2018,7 +2017,7 @@ def search_customers():
|
|
| 2018 |
if not query:
|
| 2019 |
return jsonify([])
|
| 2020 |
customers = load_json_data('customers')
|
| 2021 |
-
matches =[
|
| 2022 |
c for c in customers
|
| 2023 |
if query in c.get('name', '').lower() or query in c.get('phone', '')
|
| 2024 |
]
|
|
@@ -2064,11 +2063,13 @@ BASE_TEMPLATE = """
|
|
| 2064 |
.sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
|
| 2065 |
.sidebar.active { transform: translateX(0); }
|
| 2066 |
.main-content { margin-left: 0; }
|
| 2067 |
-
}
|
| 2068 |
-
[data-bs-theme="dark"]
|
|
|
|
| 2069 |
[data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }
|
| 2070 |
[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
|
| 2071 |
-
[data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }
|
|
|
|
| 2072 |
[data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
|
| 2073 |
.product-card { cursor: pointer; }
|
| 2074 |
.product-card:hover { border-color: var(--bs-primary); }
|
|
@@ -2087,7 +2088,7 @@ BASE_TEMPLATE = """
|
|
| 2087 |
<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>
|
| 2088 |
<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>
|
| 2089 |
<li class="nav-item dropdown">
|
| 2090 |
-
<a class="nav-link dropdown-toggle {% if request.endpoint in['reports', 'product_roi_report', 'employee_report', 'item_movement_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 2091 |
<i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
|
| 2092 |
</a>
|
| 2093 |
<ul class="dropdown-menu dropdown-menu-dark">
|
|
@@ -2099,7 +2100,7 @@ BASE_TEMPLATE = """
|
|
| 2099 |
</li>
|
| 2100 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'customer_management' %}active{% endif %}" href="{{ url_for('customer_management') }}"><i class="fas fa-fw fa-users me-2"></i>Клиенты</a></li>
|
| 2101 |
<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>
|
| 2102 |
-
<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>
|
| 2103 |
|
| 2104 |
{% if not session.admin_logged_in %}
|
| 2105 |
<li class="nav-item mt-3"><a class="nav-link text-warning" href="{{ url_for('admin_login') }}"><i class="fas fa-fw fa-user-shield me-2"></i>Войти как админ</a></li>
|
|
@@ -2338,13 +2339,40 @@ SALES_SCREEN_CONTENT = """
|
|
| 2338 |
<div id="scanner-status" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; display: none;"></div>
|
| 2339 |
<button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
|
| 2340 |
</div>
|
| 2341 |
-
|
| 2342 |
-
<input type="text" id="product-search" class="form-control mb-2" placeholder="Поиск по названию или штрих-коду...">
|
| 2343 |
-
<div id="alphabet-filter" class="d-flex flex-wrap gap-1 justify-content-end mb-3"></div>
|
| 2344 |
-
|
| 2345 |
<div id="product-search-results" class="d-grid gap-2" style="display: none; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
|
| 2346 |
|
| 2347 |
-
<div id="product-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2348 |
</div>
|
| 2349 |
</div>
|
| 2350 |
</div>
|
|
@@ -2364,7 +2392,6 @@ SALES_SCREEN_CONTENT = """
|
|
| 2364 |
</div>
|
| 2365 |
<div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
|
| 2366 |
<div class="mb-3">
|
| 2367 |
-
<div class="d-flex justify-content-between"><span>Общее кол-во:</span><span id="cart-total-qty">0 шт.</span></div>
|
| 2368 |
<div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
|
| 2369 |
<div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
|
| 2370 |
<hr class="my-1">
|
|
@@ -2476,7 +2503,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2476 |
const cartSubtotalEl = document.getElementById('cart-subtotal');
|
| 2477 |
const cartDeliveryEl = document.getElementById('cart-delivery');
|
| 2478 |
const cartTotalEl = document.getElementById('cart-total');
|
| 2479 |
-
const cartTotalQtyEl = document.getElementById('cart-total-qty');
|
| 2480 |
|
| 2481 |
let audioCtx;
|
| 2482 |
let isScannerPaused = false;
|
|
@@ -2525,63 +2551,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2525 |
});
|
| 2526 |
}
|
| 2527 |
|
| 2528 |
-
const alphabetFilterEl = document.getElementById('alphabet-filter');
|
| 2529 |
-
const productGridEl = document.getElementById('product-grid');
|
| 2530 |
-
const productSearchResultsEl = document.getElementById('product-search-results');
|
| 2531 |
-
const productSearchInput = document.getElementById('product-search');
|
| 2532 |
-
|
| 2533 |
-
const letters = new Set();
|
| 2534 |
-
allProducts.forEach(p => {
|
| 2535 |
-
if(p.name) letters.add(p.name.trim().charAt(0).toUpperCase());
|
| 2536 |
-
});
|
| 2537 |
-
const sortedLetters = Array.from(letters).sort();
|
| 2538 |
-
|
| 2539 |
-
let filterHtml = `<div class="btn-group flex-wrap" role="group">
|
| 2540 |
-
<button type="button" class="btn btn-outline-primary btn-sm alpha-filter active" data-letter="ALL">Все</button>`;
|
| 2541 |
-
sortedLetters.forEach(l => {
|
| 2542 |
-
filterHtml += `<button type="button" class="btn btn-outline-primary btn-sm alpha-filter" data-letter="${l}">${l}</button>`;
|
| 2543 |
-
});
|
| 2544 |
-
filterHtml += `</div>`;
|
| 2545 |
-
alphabetFilterEl.innerHTML = filterHtml;
|
| 2546 |
-
|
| 2547 |
-
const renderGrid = (letter) => {
|
| 2548 |
-
const filtered = letter === 'ALL' ? allProducts : allProducts.filter(p => p.name.trim().toUpperCase().startsWith(letter));
|
| 2549 |
-
productGridEl.innerHTML = filtered.map(p => {
|
| 2550 |
-
let priceText = 'Нет в наличии';
|
| 2551 |
-
if (p.variants && p.variants.length > 0) {
|
| 2552 |
-
const activeVariants = editTx ? p.variants : p.variants.filter(v => v.stock > 0);
|
| 2553 |
-
if (activeVariants.length > 0) {
|
| 2554 |
-
if (activeVariants.length === 1) {
|
| 2555 |
-
priceText = `${formatCurrencyJS(activeVariants[0].price_regular || activeVariants[0].price)} ₸`;
|
| 2556 |
-
} else {
|
| 2557 |
-
const prices = activeVariants.map(v => parseFloat(v.price_regular || v.price));
|
| 2558 |
-
priceText = `от ${formatCurrencyJS(Math.min(...prices))} ₸`;
|
| 2559 |
-
}
|
| 2560 |
-
}
|
| 2561 |
-
}
|
| 2562 |
-
return `
|
| 2563 |
-
<div class="card text-center product-card" data-barcode="${p.barcode}">
|
| 2564 |
-
<div class="card-body p-2">
|
| 2565 |
-
<h6 class="card-title small mb-1">${p.name}</h6>
|
| 2566 |
-
<p class="card-text fw-bold mb-0">${priceText}</p>
|
| 2567 |
-
</div>
|
| 2568 |
-
</div>`;
|
| 2569 |
-
}).join('');
|
| 2570 |
-
};
|
| 2571 |
-
|
| 2572 |
-
alphabetFilterEl.addEventListener('click', e => {
|
| 2573 |
-
if(e.target.classList.contains('alpha-filter')) {
|
| 2574 |
-
document.querySelectorAll('.alpha-filter').forEach(btn => btn.classList.remove('active'));
|
| 2575 |
-
e.target.classList.add('active');
|
| 2576 |
-
renderGrid(e.target.dataset.letter);
|
| 2577 |
-
productSearchInput.value = '';
|
| 2578 |
-
productGridEl.style.display = 'grid';
|
| 2579 |
-
productSearchResultsEl.style.display = 'none';
|
| 2580 |
-
}
|
| 2581 |
-
});
|
| 2582 |
-
|
| 2583 |
-
renderGrid('ALL');
|
| 2584 |
-
|
| 2585 |
function playBeep() {
|
| 2586 |
if (!audioCtx) {
|
| 2587 |
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
|
@@ -2616,14 +2585,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2616 |
const updateCartView = () => {
|
| 2617 |
cartItemsEl.innerHTML = '';
|
| 2618 |
let subtotal = 0;
|
| 2619 |
-
let totalQty = 0;
|
| 2620 |
if (Object.keys(cart).length === 0) {
|
| 2621 |
cartItemsEl.innerHTML = '<p class="text-center text-muted">Корзина пуста</p>';
|
| 2622 |
}
|
| 2623 |
for (const id in cart) {
|
| 2624 |
const item = cart[id];
|
| 2625 |
subtotal += (parseLocaleNumber(item.price) - parseLocaleNumber(item.discount)) * item.quantity;
|
| 2626 |
-
totalQty += item.quantity;
|
| 2627 |
|
| 2628 |
let nameField = '';
|
| 2629 |
let priceField = '';
|
|
@@ -2667,7 +2634,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2667 |
cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
|
| 2668 |
cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
|
| 2669 |
cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
|
| 2670 |
-
cartTotalQtyEl.textContent = totalQty + ' шт.';
|
| 2671 |
};
|
| 2672 |
|
| 2673 |
const addToCart = (product, variant, price, priceType) => {
|
|
@@ -2693,7 +2659,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2693 |
const container = document.getElementById('price-options-container');
|
| 2694 |
container.innerHTML = '';
|
| 2695 |
|
| 2696 |
-
const prices =[
|
| 2697 |
{ type: 'Общая', value: variant.price_regular || variant.price },
|
| 2698 |
{ type: 'Минимальная', value: variant.price_min },
|
| 2699 |
{ type: 'Оптовая', value: variant.price_wholesale }
|
|
@@ -2833,17 +2799,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 2833 |
});
|
| 2834 |
}
|
| 2835 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2836 |
productSearchInput.addEventListener('input', e => {
|
| 2837 |
const term = e.target.value.toLowerCase().trim();
|
| 2838 |
|
| 2839 |
if (term === '') {
|
| 2840 |
-
|
| 2841 |
productSearchResultsEl.style.display = 'none';
|
| 2842 |
productSearchResultsEl.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
| 2843 |
return;
|
| 2844 |
}
|
| 2845 |
|
| 2846 |
-
|
| 2847 |
productSearchResultsEl.style.display = 'grid';
|
| 2848 |
|
| 2849 |
const filtered = allProducts.filter(p => p.name.toLowerCase().includes(term) || p.barcode.toLowerCase().includes(term));
|
|
@@ -4653,7 +4626,7 @@ CASHIER_DASHBOARD_CONTENT = """
|
|
| 4653 |
{% endif %}
|
| 4654 |
</td>
|
| 4655 |
<td>
|
| 4656 |
-
{% if t.type == 'sale' and t.status in['completed', 'partially_returned'] %}
|
| 4657 |
<a href="{{ url_for('return_transaction', transaction_id=t.id, cashier_id=user.id) }}" class="btn btn-sm btn-warning">Возврат</a>
|
| 4658 |
{% endif %}
|
| 4659 |
</td>
|
|
@@ -4768,4 +4741,4 @@ if __name__ == '__main__':
|
|
| 4768 |
backup_thread.start()
|
| 4769 |
for key in DATA_FILES.keys():
|
| 4770 |
load_json_data(key)
|
| 4771 |
-
app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)
|
|
|
|
| 95 |
with open(filepath, 'r', encoding='utf-8') as f:
|
| 96 |
return json.load(f)
|
| 97 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 98 |
+
return []
|
| 99 |
|
| 100 |
def save_json_data(file_key, data):
|
| 101 |
filepath, lock = DATA_FILES[file_key]
|
|
|
|
| 142 |
for key in DATA_FILES.keys():
|
| 143 |
upload_db_to_hf(key)
|
| 144 |
except Exception as e:
|
| 145 |
+
logging.error(f"Error during scheduled backup: {e}", exc_info=True)
|
| 146 |
|
| 147 |
def find_item_by_field(data, field, value):
|
| 148 |
for item in data:
|
|
|
|
| 162 |
return "0"
|
| 163 |
|
| 164 |
def generate_receipt_html(transaction):
|
| 165 |
+
has_discounts = any(to_decimal(item.get('discount_per_item', '0')) > 0 for item in transaction.get('items', []))
|
| 166 |
delivery_cost = to_decimal(transaction.get('delivery_cost', '0'))
|
| 167 |
note = transaction.get('note', '')
|
| 168 |
|
|
|
|
| 187 |
total_colspan = 4
|
| 188 |
|
| 189 |
items_html = ""
|
|
|
|
| 190 |
for i, item in enumerate(transaction['items']):
|
|
|
|
| 191 |
discount_cell = f"""<td style="text-align: right;">{format_currency_py(item.get('discount_per_item', '0'))}</td>""" if has_discounts else ""
|
| 192 |
items_html += f"""
|
| 193 |
<tr>
|
|
|
|
| 202 |
|
| 203 |
total_amount_from_db = to_decimal(transaction['total_amount'])
|
| 204 |
|
| 205 |
+
totals_html = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
if delivery_cost > 0:
|
| 207 |
subtotal = total_amount_from_db - delivery_cost
|
| 208 |
totals_html += f"""
|
|
|
|
| 250 |
<title>Накладная {transaction['id'][:8]}</title>
|
| 251 |
<style>
|
| 252 |
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 10px; background-color: #f4f4f4; color: #333; }}
|
| 253 |
+
.invoice-box {{ max-width: 800px; margin: auto; padding: 20px; border: 1px solid #eee; background: white; box-shadow: 0 0 10px rgba(0,0,0,0.15); }}
|
| 254 |
.header {{ text-align: center; margin-bottom: 20px; }}
|
| 255 |
.header h1 {{ margin: 0; font-size: 22px; font-weight: 600; }}
|
| 256 |
.header p {{ margin: 2px 0; font-size: 14px; }}
|
| 257 |
.details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
|
| 258 |
table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
|
| 259 |
+
table th {{ background: #f2f2f2; font-weight: bold; padding: 8px; border-bottom: 2px solid #ddd; }}
|
| 260 |
+
table td {{ padding: 8px; border-bottom: 1px solid #eee; }}
|
| 261 |
table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
|
| 262 |
.footer-info {{ font-size: 14px; margin-top: 20px; }}
|
| 263 |
.print-hide {{ display: block; }}
|
| 264 |
@media print {{
|
| 265 |
body {{ margin: 0; padding: 0; background-color: #fff; }}
|
| 266 |
+
.invoice-box {{ box-shadow: none; border: none; margin: 0; padding: 0; }}
|
| 267 |
.print-hide {{ display: none; }}
|
| 268 |
}}
|
| 269 |
@media screen and (max-width: 600px) {{
|
|
|
|
| 287 |
<h1>Товарная накладная № {transaction['id'][:8]}</h1>
|
| 288 |
<p>от {datetime.fromisoformat(transaction['timestamp']).strftime('%d.%m.%Y %H:%M')}</p>
|
| 289 |
</div>
|
| 290 |
+
<table>
|
| 291 |
<thead>
|
| 292 |
<tr>{table_headers}</tr>
|
| 293 |
</thead>
|
| 294 |
<tbody>{items_html}</tbody>
|
| 295 |
</table>
|
| 296 |
+
<table style="margin-top: 20px;">
|
| 297 |
{totals_html}
|
| 298 |
</table>
|
| 299 |
{note_html}
|
|
|
|
| 360 |
transactions = load_json_data('transactions')
|
| 361 |
edit_tx = find_item_by_field(transactions, 'id', edit_tx_id)
|
| 362 |
|
| 363 |
+
active_inventory = []
|
| 364 |
for p in inventory:
|
| 365 |
+
if isinstance(p, dict) and any(v.get('stock', 0) > 0 for v in p.get('variants', [])):
|
| 366 |
active_inventory.append(p)
|
| 367 |
|
| 368 |
active_inventory.sort(key=lambda x: x.get('name', '').lower())
|
| 369 |
|
| 370 |
+
grouped_inventory = defaultdict(list)
|
| 371 |
+
for p in active_inventory:
|
| 372 |
+
first_letter = p.get('name', '#')[0].upper()
|
| 373 |
+
grouped_inventory[first_letter].append(p)
|
| 374 |
+
|
| 375 |
+
sorted_grouped_inventory = sorted(grouped_inventory.items())
|
| 376 |
+
|
| 377 |
html = BASE_TEMPLATE.replace('__TITLE__', "Касса").replace('__CONTENT__', SALES_SCREEN_CONTENT).replace('__SCRIPTS__', SALES_SCREEN_SCRIPTS)
|
| 378 |
+
return render_template_string(html, inventory=active_inventory, kassas=kassas, grouped_inventory=sorted_grouped_inventory, edit_tx=edit_tx)
|
| 379 |
|
| 380 |
@app.route('/inventory', methods=['GET', 'POST'])
|
| 381 |
def inventory_management():
|
|
|
|
| 395 |
flash(f"Товар со штрих-кодом {barcode} уже существует.", "warning")
|
| 396 |
return redirect(url_for('inventory_management'))
|
| 397 |
|
| 398 |
+
variants = []
|
| 399 |
variant_names = request.form.getlist('variant_name[]')
|
| 400 |
variant_prices_regular = request.form.getlist('variant_price_regular[]')
|
| 401 |
variant_prices_min = request.form.getlist('variant_price_min[]')
|
|
|
|
| 461 |
|
| 462 |
for product in inventory_list:
|
| 463 |
if isinstance(product, dict) and 'variants' in product:
|
| 464 |
+
for variant in product.get('variants', []):
|
| 465 |
stock = variant.get('stock', 0)
|
| 466 |
cost_price = to_decimal(variant.get('cost_price', '0'))
|
| 467 |
price = to_decimal(variant.get('price_regular', variant.get('price', '0')))
|
|
|
|
| 503 |
|
| 504 |
for p in inventory:
|
| 505 |
updated = False
|
| 506 |
+
for v in p.get('variants', []):
|
| 507 |
if v.get('id') in updates:
|
| 508 |
v['price_regular'] = str(to_decimal(updates[v['id']][0]))
|
| 509 |
v['price_min'] = str(to_decimal(updates[v['id']][1]))
|
|
|
|
| 541 |
inventory[i]['name'] = name
|
| 542 |
inventory[i]['barcode'] = barcode
|
| 543 |
|
| 544 |
+
new_variants = []
|
| 545 |
variant_ids = request.form.getlist('variant_id[]')
|
| 546 |
variant_names = request.form.getlist('variant_name[]')
|
| 547 |
variant_prices_regular = request.form.getlist('variant_price_regular[]')
|
|
|
|
| 588 |
def delete_product(product_id):
|
| 589 |
inventory = load_json_data('inventory')
|
| 590 |
initial_len = len(inventory)
|
| 591 |
+
inventory = [p for p in inventory if not (isinstance(p, dict) and p.get('id') == product_id)]
|
| 592 |
if len(inventory) < initial_len:
|
| 593 |
save_json_data('inventory', inventory)
|
| 594 |
upload_db_to_hf('inventory')
|
|
|
|
| 620 |
|
| 621 |
variant_found = False
|
| 622 |
variant_name_for_log = ""
|
| 623 |
+
for i, variant in enumerate(product.get('variants', [])):
|
| 624 |
if variant.get('id') == variant_id:
|
| 625 |
variant_name_for_log = variant.get('option_value', '')
|
| 626 |
|
|
|
|
| 704 |
inventory = load_json_data('inventory')
|
| 705 |
product = find_item_by_field(inventory, 'barcode', barcode)
|
| 706 |
if product:
|
| 707 |
+
active_variants = [v for v in product.get('variants', []) if v.get('stock', 0) > 0]
|
| 708 |
if active_variants:
|
| 709 |
product_copy = product.copy()
|
| 710 |
product_copy['variants'] = active_variants
|
|
|
|
| 734 |
if not original_tx:
|
| 735 |
return jsonify({'success': False, 'message': 'Оригинальная накладная не найдена.'}), 404
|
| 736 |
|
| 737 |
+
for item in original_tx.get('items', []):
|
| 738 |
if not item.get('is_custom'):
|
| 739 |
for p in inventory:
|
| 740 |
if p.get('id') == item.get('product_id'):
|
| 741 |
+
for v in p.get('variants', []):
|
| 742 |
if v.get('id') == item.get('variant_id'):
|
| 743 |
v['stock'] = v.get('stock', 0) + item.get('quantity', 0)
|
| 744 |
break
|
|
|
|
| 750 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 751 |
amount = to_decimal(original_tx.get('total_amount', '0'))
|
| 752 |
k['balance'] = str(current_balance - amount)
|
| 753 |
+
k.setdefault('history', []).append({
|
| 754 |
'type': 'correction_revert',
|
| 755 |
'amount': str(-amount),
|
| 756 |
'timestamp': get_current_time().isoformat(),
|
|
|
|
| 774 |
if not user or not kassa:
|
| 775 |
return jsonify({'success': False, 'message': 'Кассир или касса были удалены. Требуется повторный вход.', 'logout_required': True}), 401
|
| 776 |
|
| 777 |
+
sale_items = []
|
| 778 |
items_total = Decimal('0')
|
| 779 |
inventory_updates = {}
|
| 780 |
|
|
|
|
| 803 |
if not product:
|
| 804 |
return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404
|
| 805 |
|
| 806 |
+
variant = find_item_by_field(product.get('variants', []), 'id', variant_id)
|
| 807 |
if not variant:
|
| 808 |
return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404
|
| 809 |
|
|
|
|
| 877 |
for variant_id, update_info in inventory_updates.items():
|
| 878 |
for p in inventory:
|
| 879 |
if p.get('id') == update_info['product_id']:
|
| 880 |
+
for v in p.get('variants', []):
|
| 881 |
if v.get('id') == variant_id:
|
| 882 |
v['stock'] = update_info['new_stock']
|
| 883 |
p['timestamp_updated'] = now_iso
|
|
|
|
| 944 |
transactions = load_json_data('transactions')
|
| 945 |
kassas = load_json_data('kassas')
|
| 946 |
|
| 947 |
+
filtered_transactions = [
|
| 948 |
t for t in transactions
|
| 949 |
if datetime.fromisoformat(t['timestamp']).date() == selected_date
|
| 950 |
]
|
| 951 |
|
| 952 |
if selected_kassa_id:
|
| 953 |
+
filtered_transactions = [
|
| 954 |
t for t in filtered_transactions
|
| 955 |
if t.get('kassa_id') == selected_kassa_id
|
| 956 |
]
|
|
|
|
| 960 |
total_quantity_sold = 0
|
| 961 |
for t in filtered_transactions:
|
| 962 |
if t.get('type') == 'sale':
|
| 963 |
+
for item in t.get('items', []):
|
| 964 |
total_quantity_sold += int(item.get('quantity', 0))
|
| 965 |
|
| 966 |
filtered_transactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
|
|
| 973 |
def edit_transaction(transaction_id):
|
| 974 |
try:
|
| 975 |
data = request.get_json()
|
| 976 |
+
items_update = data.get('items', [])
|
| 977 |
|
| 978 |
transactions = load_json_data('transactions')
|
| 979 |
kassas = load_json_data('kassas')
|
|
|
|
| 991 |
old_total_amount = to_decimal(original_transaction['total_amount'])
|
| 992 |
|
| 993 |
new_items_total = Decimal('0')
|
| 994 |
+
updated_items = []
|
| 995 |
|
| 996 |
for item in original_transaction['items']:
|
| 997 |
item_id = item.get('variant_id') or item.get('product_id')
|
|
|
|
| 1013 |
transactions[transaction_index]['total_amount'] = str(new_total_amount)
|
| 1014 |
|
| 1015 |
if 'edits' not in transactions[transaction_index]:
|
| 1016 |
+
transactions[transaction_index]['edits'] = []
|
| 1017 |
|
| 1018 |
transactions[transaction_index]['edits'].append({
|
| 1019 |
'timestamp': get_current_time().isoformat(),
|
|
|
|
| 1030 |
for i, k in enumerate(kassas):
|
| 1031 |
if k.get('id') == kassa_id:
|
| 1032 |
k['balance'] = str(to_decimal(k.get('balance', '0')) + amount_diff)
|
| 1033 |
+
k.setdefault('history', []).append({
|
| 1034 |
'type': 'correction',
|
| 1035 |
'amount': str(amount_diff),
|
| 1036 |
'timestamp': get_current_time().isoformat(),
|
|
|
|
| 1061 |
flash("Транзакция не найдена.", "danger")
|
| 1062 |
return redirect(url_for('transaction_history'))
|
| 1063 |
|
| 1064 |
+
for item in transaction_to_delete.get('items', []):
|
| 1065 |
if item.get('is_custom'):
|
| 1066 |
continue
|
| 1067 |
|
| 1068 |
product = find_item_by_field(inventory, 'id', item.get('product_id'))
|
| 1069 |
if not product: continue
|
| 1070 |
|
| 1071 |
+
variant = find_item_by_field(product.get('variants', []), 'id', item.get('variant_id'))
|
| 1072 |
if not variant: continue
|
| 1073 |
|
| 1074 |
quantity_change = item.get('quantity', 0)
|
|
|
|
| 1086 |
|
| 1087 |
kassa['balance'] = str(current_balance - amount_change)
|
| 1088 |
|
| 1089 |
+
kassa.setdefault('history', []).append({
|
| 1090 |
'type': 'deletion',
|
| 1091 |
'amount': str(-amount_change),
|
| 1092 |
'timestamp': get_current_time().isoformat(),
|
|
|
|
| 1130 |
personal_expenses = load_json_data('personal_expenses')
|
| 1131 |
users = load_json_data('users')
|
| 1132 |
|
| 1133 |
+
filtered_transactions = [
|
| 1134 |
t for t in transactions
|
| 1135 |
if start_date <= datetime.fromisoformat(t['timestamp']) < end_date
|
| 1136 |
]
|
| 1137 |
+
filtered_expenses = [
|
| 1138 |
e for e in expenses
|
| 1139 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 1140 |
]
|
| 1141 |
+
filtered_personal_expenses = [
|
| 1142 |
e for e in personal_expenses
|
| 1143 |
if start_date <= datetime.fromisoformat(e['timestamp']) < end_date
|
| 1144 |
]
|
| 1145 |
|
| 1146 |
+
return_transactions = [t for t in filtered_transactions if t.get('type') == 'return']
|
| 1147 |
total_returns_amount = sum(to_decimal(t['total_amount']) for t in return_transactions)
|
| 1148 |
total_returns_count = len(return_transactions)
|
| 1149 |
|
|
|
|
| 1222 |
end_date = (datetime.strptime(end_date_str, '%Y-%m-%d') + timedelta(days=1)).replace(tzinfo=ALMATY_TZ)
|
| 1223 |
|
| 1224 |
transactions = load_json_data('transactions')
|
| 1225 |
+
employee_transactions = []
|
| 1226 |
totals = {'sales': Decimal(0), 'returns': Decimal(0), 'net': Decimal(0)}
|
| 1227 |
|
| 1228 |
if user_id:
|
|
|
|
| 1254 |
product_id = request.args.get('product_id', '')
|
| 1255 |
variant_id = request.args.get('variant_id', '')
|
| 1256 |
|
| 1257 |
+
movements = []
|
| 1258 |
selected_product = None
|
| 1259 |
selected_variant = None
|
| 1260 |
|
| 1261 |
if product_id:
|
| 1262 |
selected_product = find_item_by_field(inventory, 'id', product_id)
|
| 1263 |
if selected_product and variant_id:
|
| 1264 |
+
selected_variant = find_item_by_field(selected_product.get('variants', []), 'id', variant_id)
|
| 1265 |
|
| 1266 |
for t in transactions:
|
| 1267 |
+
for item in t.get('items', []):
|
| 1268 |
if item.get('product_id') == product_id and (not variant_id or item.get('variant_id') == variant_id):
|
| 1269 |
movements.append({
|
| 1270 |
'timestamp': t['timestamp'],
|
|
|
|
| 1299 |
inventory = load_json_data('inventory')
|
| 1300 |
transactions = load_json_data('transactions')
|
| 1301 |
|
| 1302 |
+
product_stats = []
|
| 1303 |
|
| 1304 |
for product in inventory:
|
| 1305 |
+
for variant in product.get('variants', []):
|
| 1306 |
total_revenue = Decimal('0')
|
| 1307 |
total_cogs = Decimal('0')
|
| 1308 |
total_qty_sold = 0
|
|
|
|
| 1397 |
elif action == 'delete':
|
| 1398 |
user_id = request.form.get('id')
|
| 1399 |
initial_len = len(users)
|
| 1400 |
+
users = [u for u in users if u.get('id') != user_id]
|
| 1401 |
if len(users) < initial_len:
|
| 1402 |
flash("Кассир удален.", "success")
|
| 1403 |
else:
|
|
|
|
| 1421 |
'id': uuid.uuid4().hex,
|
| 1422 |
'name': name,
|
| 1423 |
'balance': str(balance),
|
| 1424 |
+
'history': []
|
| 1425 |
}
|
| 1426 |
if balance > 0:
|
| 1427 |
new_kassa['history'].append({
|
|
|
|
| 1438 |
elif action == 'delete':
|
| 1439 |
kassa_id = request.form.get('id')
|
| 1440 |
initial_len = len(kassas)
|
| 1441 |
+
kassas = [k for k in kassas if k.get('id') != kassa_id]
|
| 1442 |
if len(kassas) < initial_len:
|
| 1443 |
flash("Касса удалена.", "success")
|
| 1444 |
else:
|
|
|
|
| 1523 |
def delete_expense(expense_id):
|
| 1524 |
expenses = load_json_data('expenses')
|
| 1525 |
initial_len = len(expenses)
|
| 1526 |
+
expenses = [e for e in expenses if e.get('id') != expense_id]
|
| 1527 |
if len(expenses) < initial_len:
|
| 1528 |
save_json_data('expenses', expenses)
|
| 1529 |
upload_db_to_hf('expenses')
|
|
|
|
| 1560 |
def delete_personal_expense(expense_id):
|
| 1561 |
expenses = load_json_data('personal_expenses')
|
| 1562 |
initial_len = len(expenses)
|
| 1563 |
+
expenses = [e for e in expenses if e.get('id') != expense_id]
|
| 1564 |
if len(expenses) < initial_len:
|
| 1565 |
save_json_data('personal_expenses', expenses)
|
| 1566 |
upload_db_to_hf('personal_expenses')
|
|
|
|
| 1582 |
flash("Ссылка добавлена.", "success")
|
| 1583 |
elif action == 'delete':
|
| 1584 |
link_id = request.form.get('id')
|
| 1585 |
+
links = [l for l in links if l.get('id') != link_id]
|
| 1586 |
flash("Ссылка удалена.", "success")
|
| 1587 |
save_json_data('links', links)
|
| 1588 |
upload_db_to_hf('links')
|
|
|
|
| 1679 |
|
| 1680 |
shift['end_balance'] = kassa.get('balance', '0') if kassa else '0'
|
| 1681 |
|
| 1682 |
+
shift_transactions = [
|
| 1683 |
t for t in transactions
|
| 1684 |
if t.get('shift_id') == shift_id and datetime.fromisoformat(t['timestamp']) >= datetime.fromisoformat(shift['start_time'])
|
| 1685 |
]
|
|
|
|
| 1710 |
abort(404, "Кассир не найден")
|
| 1711 |
|
| 1712 |
transactions = load_json_data('transactions')
|
| 1713 |
+
user_transactions = [t for t in transactions if t.get('user_id') == user_id]
|
| 1714 |
user_transactions.sort(key=lambda x: x['timestamp'], reverse=True)
|
| 1715 |
|
| 1716 |
html = BASE_TEMPLATE.replace('__TITLE__', f"Продажи кассира: {user['name']}").replace('__CONTENT__', CASHIER_DASHBOARD_CONTENT).replace('__SCRIPTS__', '')
|
|
|
|
| 1740 |
flash("Не указан ID кассира.", "danger")
|
| 1741 |
return redirect(url_for('cashier_login'))
|
| 1742 |
|
| 1743 |
+
returnable_items = []
|
| 1744 |
already_returned = original_transaction.get('return_info', {}).get('returned_items', {})
|
| 1745 |
|
| 1746 |
for item in original_transaction['items']:
|
|
|
|
| 1761 |
flash("Не удалось определить кассира.", "danger")
|
| 1762 |
return redirect(url_for('cashier_login'))
|
| 1763 |
|
| 1764 |
+
return_items = []
|
| 1765 |
total_return_amount = Decimal('0')
|
| 1766 |
inventory_updates = {}
|
| 1767 |
items_to_process = defaultdict(int)
|
|
|
|
| 1806 |
if not item.get('is_custom'):
|
| 1807 |
product = find_item_by_field(inventory, 'id', item['product_id'])
|
| 1808 |
if product:
|
| 1809 |
+
variant = find_item_by_field(product.get('variants', []), 'id', variant_id)
|
| 1810 |
if variant:
|
| 1811 |
inventory_updates[variant_id] = {'product_id': item['product_id'], 'stock_change': qty_to_return}
|
| 1812 |
|
|
|
|
| 1853 |
if k.get('id') == original_transaction['kassa_id']:
|
| 1854 |
current_balance = to_decimal(k.get('balance', '0'))
|
| 1855 |
kassas[k_idx]['balance'] = str(current_balance - total_return_amount)
|
| 1856 |
+
kassas[k_idx].setdefault('history', []).append({
|
| 1857 |
'type': 'return', 'amount': str(-total_return_amount), 'timestamp': now_iso,
|
| 1858 |
'transaction_id': return_transaction_id
|
| 1859 |
})
|
|
|
|
| 1883 |
@app.route('/download', methods=['GET'])
|
| 1884 |
@admin_required
|
| 1885 |
def download_hf():
|
| 1886 |
+
errors = []
|
| 1887 |
success_count = 0
|
| 1888 |
for key in DATA_FILES.keys():
|
| 1889 |
filepath, _ = DATA_FILES[key]
|
|
|
|
| 1954 |
if not bill_to_restore:
|
| 1955 |
return jsonify({'success': False, 'message': 'Bill not found'}), 404
|
| 1956 |
|
| 1957 |
+
remaining_bills = [b for b in bills if b.get('id') != bill_id]
|
| 1958 |
save_json_data('held_bills', remaining_bills)
|
| 1959 |
upload_db_to_hf('held_bills')
|
| 1960 |
return jsonify({'success': True, 'bill': bill_to_restore})
|
|
|
|
| 2001 |
elif action == 'delete':
|
| 2002 |
customer_id = request.form.get('id')
|
| 2003 |
initial_len = len(customers)
|
| 2004 |
+
customers = [c for c in customers if c.get('id') != customer_id]
|
| 2005 |
if len(customers) < initial_len:
|
| 2006 |
flash("Клиент удален.", "success")
|
| 2007 |
else:
|
|
|
|
| 2017 |
if not query:
|
| 2018 |
return jsonify([])
|
| 2019 |
customers = load_json_data('customers')
|
| 2020 |
+
matches = [
|
| 2021 |
c for c in customers
|
| 2022 |
if query in c.get('name', '').lower() or query in c.get('phone', '')
|
| 2023 |
]
|
|
|
|
| 2063 |
.sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; }
|
| 2064 |
.sidebar.active { transform: translateX(0); }
|
| 2065 |
.main-content { margin-left: 0; }
|
| 2066 |
+
}
|
| 2067 |
+
[data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; }
|
| 2068 |
+
[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; }
|
| 2069 |
[data-bs-theme="dark"] .accordion-button { background-color: #3e444a; color: #fff; }
|
| 2070 |
[data-bs-theme="dark"] .accordion-button:not(.collapsed) { background-color: #495057;}
|
| 2071 |
+
[data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); }
|
| 2072 |
+
[data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); }
|
| 2073 |
[data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; }
|
| 2074 |
.product-card { cursor: pointer; }
|
| 2075 |
.product-card:hover { border-color: var(--bs-primary); }
|
|
|
|
| 2088 |
<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>
|
| 2089 |
<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>
|
| 2090 |
<li class="nav-item dropdown">
|
| 2091 |
+
<a class="nav-link dropdown-toggle {% if request.endpoint in ['reports', 'product_roi_report', 'employee_report', 'item_movement_report'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 2092 |
<i class="fas fa-fw fa-chart-line me-2"></i>Отчеты
|
| 2093 |
</a>
|
| 2094 |
<ul class="dropdown-menu dropdown-menu-dark">
|
|
|
|
| 2100 |
</li>
|
| 2101 |
<li class="nav-item"><a class="nav-link {% if request.endpoint == 'customer_management' %}active{% endif %}" href="{{ url_for('customer_management') }}"><i class="fas fa-fw fa-users me-2"></i>Клиенты</a></li>
|
| 2102 |
<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>
|
| 2103 |
+
<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>
|
| 2104 |
|
| 2105 |
{% if not session.admin_logged_in %}
|
| 2106 |
<li class="nav-item mt-3"><a class="nav-link text-warning" href="{{ url_for('admin_login') }}"><i class="fas fa-fw fa-user-shield me-2"></i>Войти как админ</a></li>
|
|
|
|
| 2339 |
<div id="scanner-status" style="position: absolute; top: 10px; left: 10px; background: rgba(0,0,0,0.5); color: white; padding: 5px; border-radius: 5px; display: none;"></div>
|
| 2340 |
<button id="stop-scan-btn" class="btn btn-danger btn-sm mt-2">Остановить</button>
|
| 2341 |
</div>
|
| 2342 |
+
<input type="text" id="product-search" class="form-control mb-3" placeholder="Поиск по названию или штрих-коду...">
|
|
|
|
|
|
|
|
|
|
| 2343 |
<div id="product-search-results" class="d-grid gap-2" style="display: none; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));"></div>
|
| 2344 |
|
| 2345 |
+
<div id="product-accordion" class="accordion">
|
| 2346 |
+
{% for letter, products in grouped_inventory %}
|
| 2347 |
+
<div class="accordion-item">
|
| 2348 |
+
<h2 class="accordion-header" id="heading-{{ letter }}">
|
| 2349 |
+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-{{ letter }}" aria-expanded="false" aria-controls="collapse-{{ letter }}">
|
| 2350 |
+
{{ letter }}
|
| 2351 |
+
</button>
|
| 2352 |
+
</h2>
|
| 2353 |
+
<div id="collapse-{{ letter }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ letter }}" data-bs-parent="#product-accordion">
|
| 2354 |
+
<div class="accordion-body d-grid gap-2" style="grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));">
|
| 2355 |
+
{% for p in products %}
|
| 2356 |
+
<div class="card text-center product-card" data-barcode="{{ p.barcode }}">
|
| 2357 |
+
<div class="card-body p-2">
|
| 2358 |
+
<h6 class="card-title small mb-1">{{ p.name }}</h6>
|
| 2359 |
+
<p class="card-text fw-bold mb-0">
|
| 2360 |
+
{% if p.variants|length > 1 %}
|
| 2361 |
+
от {{ format_currency_py(p.variants|map(attribute='price_regular')|min) }} ₸
|
| 2362 |
+
{% elif p.variants|length == 1 %}
|
| 2363 |
+
{{ format_currency_py(p.variants[0].get('price_regular', p.variants[0].get('price'))) }} ₸
|
| 2364 |
+
{% else %}
|
| 2365 |
+
Нет в наличии
|
| 2366 |
+
{% endif %}
|
| 2367 |
+
</p>
|
| 2368 |
+
</div>
|
| 2369 |
+
</div>
|
| 2370 |
+
{% endfor %}
|
| 2371 |
+
</div>
|
| 2372 |
+
</div>
|
| 2373 |
+
</div>
|
| 2374 |
+
{% endfor %}
|
| 2375 |
+
</div>
|
| 2376 |
</div>
|
| 2377 |
</div>
|
| 2378 |
</div>
|
|
|
|
| 2392 |
</div>
|
| 2393 |
<div id="cart-items" class="list-group mb-3" style="max-height: 400px; overflow-y: auto;"></div>
|
| 2394 |
<div class="mb-3">
|
|
|
|
| 2395 |
<div class="d-flex justify-content-between"><span>Подытог:</span><span id="cart-subtotal">0 ₸</span></div>
|
| 2396 |
<div class="d-flex justify-content-between"><span>Доставка:</span><span id="cart-delivery">0 ₸</span></div>
|
| 2397 |
<hr class="my-1">
|
|
|
|
| 2503 |
const cartSubtotalEl = document.getElementById('cart-subtotal');
|
| 2504 |
const cartDeliveryEl = document.getElementById('cart-delivery');
|
| 2505 |
const cartTotalEl = document.getElementById('cart-total');
|
|
|
|
| 2506 |
|
| 2507 |
let audioCtx;
|
| 2508 |
let isScannerPaused = false;
|
|
|
|
| 2551 |
});
|
| 2552 |
}
|
| 2553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2554 |
function playBeep() {
|
| 2555 |
if (!audioCtx) {
|
| 2556 |
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
|
|
|
|
| 2585 |
const updateCartView = () => {
|
| 2586 |
cartItemsEl.innerHTML = '';
|
| 2587 |
let subtotal = 0;
|
|
|
|
| 2588 |
if (Object.keys(cart).length === 0) {
|
| 2589 |
cartItemsEl.innerHTML = '<p class="text-center text-muted">Корзина пуста</p>';
|
| 2590 |
}
|
| 2591 |
for (const id in cart) {
|
| 2592 |
const item = cart[id];
|
| 2593 |
subtotal += (parseLocaleNumber(item.price) - parseLocaleNumber(item.discount)) * item.quantity;
|
|
|
|
| 2594 |
|
| 2595 |
let nameField = '';
|
| 2596 |
let priceField = '';
|
|
|
|
| 2634 |
cartSubtotalEl.textContent = formatCurrencyJS(subtotal) + ' ₸';
|
| 2635 |
cartDeliveryEl.textContent = formatCurrencyJS(deliveryCost) + ' ₸';
|
| 2636 |
cartTotalEl.textContent = formatCurrencyJS(total) + ' ₸';
|
|
|
|
| 2637 |
};
|
| 2638 |
|
| 2639 |
const addToCart = (product, variant, price, priceType) => {
|
|
|
|
| 2659 |
const container = document.getElementById('price-options-container');
|
| 2660 |
container.innerHTML = '';
|
| 2661 |
|
| 2662 |
+
const prices = [
|
| 2663 |
{ type: 'Общая', value: variant.price_regular || variant.price },
|
| 2664 |
{ type: 'Минимальная', value: variant.price_min },
|
| 2665 |
{ type: 'Оптовая', value: variant.price_wholesale }
|
|
|
|
| 2799 |
});
|
| 2800 |
}
|
| 2801 |
|
| 2802 |
+
const productSearchInput = document.getElementById('product-search');
|
| 2803 |
+
const productAccordionEl = document.getElementById('product-accordion');
|
| 2804 |
+
const productSearchResultsEl = document.getElementById('product-search-results');
|
| 2805 |
+
|
| 2806 |
productSearchInput.addEventListener('input', e => {
|
| 2807 |
const term = e.target.value.toLowerCase().trim();
|
| 2808 |
|
| 2809 |
if (term === '') {
|
| 2810 |
+
productAccordionEl.style.display = '';
|
| 2811 |
productSearchResultsEl.style.display = 'none';
|
| 2812 |
productSearchResultsEl.innerHTML = '';
|
| 2813 |
+
document.querySelectorAll('#product-accordion .accordion-collapse.show').forEach(el => {
|
| 2814 |
+
bootstrap.Collapse.getOrCreateInstance(el).hide();
|
| 2815 |
+
});
|
| 2816 |
return;
|
| 2817 |
}
|
| 2818 |
|
| 2819 |
+
productAccordionEl.style.display = 'none';
|
| 2820 |
productSearchResultsEl.style.display = 'grid';
|
| 2821 |
|
| 2822 |
const filtered = allProducts.filter(p => p.name.toLowerCase().includes(term) || p.barcode.toLowerCase().includes(term));
|
|
|
|
| 4626 |
{% endif %}
|
| 4627 |
</td>
|
| 4628 |
<td>
|
| 4629 |
+
{% if t.type == 'sale' and t.status in ['completed', 'partially_returned'] %}
|
| 4630 |
<a href="{{ url_for('return_transaction', transaction_id=t.id, cashier_id=user.id) }}" class="btn btn-sm btn-warning">Возврат</a>
|
| 4631 |
{% endif %}
|
| 4632 |
</td>
|
|
|
|
| 4741 |
backup_thread.start()
|
| 4742 |
for key in DATA_FILES.keys():
|
| 4743 |
load_json_data(key)
|
| 4744 |
+
app.run(debug=False, host='0.0.0.0', port=7860, use_reloader=False)
|